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
31 changes: 31 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"permissions": {
"allow": [
"Bash(venv/bin/python -m pytest:*)",
"Bash(venv/bin/python scripts/:*)",
"Bash(venv/bin/python src/ingest_standards.py)",
"Bash(venv/bin/pip install -r requirements.txt)",
"Bash(venv/bin/uvicorn:*)",
"Bash(uv venv:*)",
"Bash(npm run dev:*)",
"Bash(npm run build:*)",
"Bash(npm run type-check:*)",
"Bash(npm run format:*)",
"Bash(npm run test:e2e:*)",
"Bash(npx playwright test:*)",
"Bash(npx playwright install chromium)",
"Bash(git status:*)",
"Bash(git diff:*)",
"Bash(git log:*)",
"Bash(git show:*)",
"Bash(git fetch:*)",
"Bash(git add:*)",
"Bash(git commit:*)",
"Bash(git branch:*)",
"Bash(git checkout -b:*)",
"Bash(gh pr view:*)",
"Bash(gh pr checks:*)",
"Bash(gh pr list:*)"
]
}
}
75 changes: 75 additions & 0 deletions .claude/skills/feature/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
name: feature
description: Take a feature request through the full pipeline — explore, plan, implement on a branch, test, and open a PR. Use when the user requests a new feature or behavior change in the app.
---

# Feature pipeline

Input: a feature request, possibly one line. Output: a reviewed-ready PR with tests.

## 1. Scope

Restate the request in one sentence. If it is genuinely ambiguous (not just underspecified in ways
sensible defaults cover), ask **one** clarifying question; otherwise proceed.

## 2. Explore

Find the closest existing analog and mirror its structure rather than inventing new patterns:
- New endpoint → nearest endpoint in `src/main.py` + its test in `tests/test_*_api.py`
- New UI behavior → nearest page/component under `frontend/app/` + its spec in `frontend/e2e/`
- New worksheet type → existing type in `src/worksheets/` + `factory.py` registration + AGENTS.md §3

## 3. Plan

Write a short plan before coding: files to touch, tests to add, done-criteria. For non-trivial
features, show the plan to the user before implementing.

## 4. Branch

```bash
git fetch origin
git checkout -b feat/<kebab-slug> origin/main # or fix/<kebab-slug>
```
Follow AGENTS.md §7 (never commit to `main`; verify the branch isn't already merged before pushing).

## 5. Implement

Match surrounding style. Before committing, on every file you touched:
`venv/bin/python -m ruff check --fix <files> && venv/bin/python -m black <files>` for Python,
`npm run format` for frontend files. Pre-commit hooks enforce these.

## 6. Test policy — required, within reason

- Each new backend behavior gets a pytest test (use `tests/factories.py` builders and
`tests/conftest.py` fixtures; temp DBs only).
- Each UI-facing use case gets Playwright coverage of its **happy path** in the relevant
`frontend/e2e/*.spec.ts` (seeding helpers in `e2e/fixtures/api.ts`). Cover the use case's
correctness, **not** exhaustive boundary matrices.
- No new test frameworks. Ad-hoc lesson content (`/generate-week`) is exempt from this policy.

## 7. Verify

Use the `/test` skill. While iterating, run single files/specs; before the PR, all of:

```bash
venv/bin/python -m pytest tests/ -v --tb=short
cd frontend && npm run type-check && npm run format:check && npm run test:e2e
```

All green before opening the PR. Report failures verbatim — never weaken an existing test to pass.
If a check fails, confirm it isn't pre-existing (`git stash && <re-run check> && git stash pop`):
report pre-existing failures to the user separately instead of bundling fixes into your PR.

## 8. PR

```bash
git push -u origin <branch>
gh pr create --head <branch> --base main --title "feat: <summary>" --body "..."
```

(`--head`/`--base` are required when the working tree has unrelated uncommitted files —
without them `gh` may refuse to pick a branch.)

- Conventional-commit title (`feat:` / `fix:`).
- Body: what & why, test evidence (suite counts), `Closes #N` when an issue exists.
- End the body with the standard Claude Code attribution footer.
137 changes: 137 additions & 0 deletions .claude/skills/generate-week/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
---
name: generate-week
description: Author a themed 5-day printable lesson week offline — Claude writes all content (no OpenAI key), renders via the HTML packet engine, and emits a student packet + teacher guide. Use when asked to generate a lesson week, unit, or worksheet packet ad hoc.
---

# Generate a themed lesson week

You author every passage, question, and worksheet yourself — there is no LLM API call. Quality of
the written content is the whole product; budget most of your effort there, not on plumbing.

## 0. Required reading

Read `AGENTS.md` first. You will need:
- §3 — the worksheet-type table: **only HTML-capable kinds** (camelCase, e.g. `readingWorksheet`)
can go in a print packet, and the `feature_matrix` items-format gotcha (HTML wants plain strings).
- §2 — the "Instructional Pair" pattern (reading page + application page).
- §4 — pedagogy: "LET'S DISCUSS" closer with `response_lines: 0`, shuffled word banks/vocabulary,
outdoor activity in `instructions`, and a consistent narrator character introduced on Day 1.

Then skim `scripts/generate_weather_week_series.py` — the tracked canonical example of this skill's
full output, including the embedded teacher guide.

**Exact field names per kind are NOT all documented in AGENTS.md.** Before writing data dicts for a
kind you haven't used, read its render function in `src/worksheet_html_renderer.py` (e.g.
`causeEffectWorksheet` takes `pairs` with `cause`/`effect`/`effect_lines`; `treeMapWorksheet`
supports an optional `columns` layout key; `matchingWorksheet` items may be strings or
`{"text": ...}` dicts).

## 1. Inputs

- **Theme** (required) — e.g. "weather", "ancient Egypt".
- Optional: age/grade, subject, standards. Default to the established audience (Christopher,
age 6, grade K–1) if unspecified. If asked to align to standards, look them up in
`standards_data/` or `curriculum.db`.

## 2. Design the week (before writing code)

- A 5-day **causal arc** — each day's concept builds on the previous (e.g. Sun → tilt → seasons →
climate → biomes). Write the arc as one line; it goes in the script docstring.
- Per day: an **instructional pair** — a `readingWorksheet` plus one application worksheet
(featureMatrix / treeMap / causeEffect / wordSort / matching / oddOneOut / tChart / frayerModel).
Vary the application kinds across the week. Keep feature matrices to ≤6 properties so the
printed table stays readable.
- Friday: a capstone that synthesizes the whole arc.
- Final page: a **Parent Feedback & Teaching Notes** page (a `readingWorksheet` whose questions ask
the parent about comprehension, curiosity, and topics to revisit).
- Annotate each day with standards IDs in the docstring when standards were given.

## 3. Write the script

Create `scripts/generate_<theme>_week_series.py`. Canonical skeleton (ruff already exempts
`scripts/*.py` from E402, so the `os.chdir` preamble is fine):

```python
"""
<Theme> — Week Series
Grade <X> | <Subject> | Causal Arc: <one line>

Standards:
Monday — <ids> (<concept>)
...
"""

import os

os.chdir(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import sys
from pathlib import Path

sys.path.insert(0, os.path.abspath("src"))

from worksheet_html_renderer import build_print_packet_html, render_worksheet_html


def generate_<theme>_week_series():
output_dir = Path("<theme>_week_series")
output_dir.mkdir(exist_ok=True)
pages: list[tuple[str, str]] = []

def add(kind: str, data: dict, day_label: str) -> None:
fragment = render_worksheet_html(kind, data, day_label)
if fragment is None:
raise ValueError(f"No HTML renderer for kind={kind!r}")
pages.append((day_label, fragment))

# MONDAY ... FRIDAY: add(...) calls, then the parent-feedback page

html = build_print_packet_html(pages, packet_title="<Theme> Week — <subtitle>")
(output_dir / "<theme>_week.html").write_text(html, encoding="utf-8")

TEACHER_GUIDE = """<!DOCTYPE html>...""" # see step 4
(output_dir / "<theme>_week_teacher_guide.html").write_text(TEACHER_GUIDE, encoding="utf-8")

print(f"Generated {len(pages)} pages -> {output_dir}/")
# then print a one-line label per page (the manifest)


if __name__ == "__main__":
generate_<theme>_week_series()
```

`scripts/generate_weather_week_series.py` is the tracked, canonical realization of this skeleton.
`scripts/reference/` (gitignored, may not exist on every machine) holds older bespoke one-offs —
skimmable if present, but not canonical.

## 4. Teacher guide — emitted by the same script

Output directories are gitignored; the script is the only tracked artifact and must reproduce the
entire week. Embed the guide as an HTML string in the script and write it alongside the packet.
Contents per day: answer key for every question/cell, guidance for the "LET'S DISCUSS" prompt,
misconceptions to watch for, and one extension activity. Model the document structure and CSS on
the teacher guide embedded in `scripts/generate_weather_week_series.py` (self-contained HTML,
day-color headings matching the packet palette).

## 5. Run and validate

```bash
venv/bin/python scripts/generate_<theme>_week_series.py
```

- Exit 0 (the `add()` helper raises on any unknown kind — an exception means a bad kind or field).
- Printed page count matches your design (typically 11: 5×2 + feedback page).
- Both HTML files exist and are non-trivially sized (packet is typically 40–60 KB).
- Read back one or two fragments from the packet HTML to spot-check content landed in the right
fields (e.g. vocabulary in the sidebar, `response_lines: 0` rendered without write-in lines).
- Tell the user to open the packet in a browser — the print dialog opens automatically.

**No tests are required for generated weeks** — the validation above is sufficient.

## 6. Commit

Before committing, run `venv/bin/python -m ruff check --fix` and `venv/bin/python -m black` on the
script (pre-commit enforces both; f-strings without placeholders are a common miss), then re-run
the script once to confirm it still generates.

Commit the script only (outputs are gitignored), on a branch with a PR per AGENTS.md §7:
`feat: <theme> week — <arc summary>`.
49 changes: 49 additions & 0 deletions .claude/skills/test/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
name: test
description: Run and interpret the backend pytest suite and the Playwright E2E suite. Use for regression checks, before any PR, or when asked to run tests.
---

# Running the test suites

Both suites must pass before any PR. Run from the repo root unless noted.

## Backend (pytest)

```bash
venv/bin/python -m pytest tests/ -v --tb=short # full suite (~135 tests, ~3s)
venv/bin/python -m pytest tests/test_feedback_api.py # one file
venv/bin/python -m pytest tests/ -k "cooldown" # by keyword
```

- Fixtures (`tests/conftest.py`) create temp SQLite DBs — tests never touch `curriculum.db`.
- Test-data builders live in `tests/factories.py`; prefer them over hand-rolled dicts.
- On failure: report the failing assertion verbatim, then fix or escalate — do not skip or
loosen an assertion to get green.

## E2E (Playwright)

```bash
cd frontend
npm run test:e2e # full suite (~41 tests, <1 min)
npx playwright test e2e/feedback.spec.ts # one spec
npx playwright test --grep "Edit Student" # one test by title
npx playwright test --headed # watch the browser
npm run test:e2e:ui # interactive UI mode
npm run test:e2e:report # open last HTML report
```

- Self-contained: `global-setup.ts` spawns `venv/bin/uvicorn` on port 8182 with an isolated DB
at `/tmp/playwright-test.db`; the frontend test server runs on port 3002. Dev servers and
`curriculum.db` are untouched.
- Requires the repo-root `venv/` and `npx playwright install chromium` (see CLAUDE.md bootstrap).
- Seeding helpers for new tests: `frontend/e2e/fixtures/api.ts` (`createStudent`, `createPacket`,
`submitFeedback`, `backdateFeedback`).

### Troubleshooting

- **Port 8182 or 3002 in use**: a previous run left processes behind — kill stale `uvicorn`
(`/tmp/playwright-backend.pid`) and `next dev` processes, then re-run.
- **Failure triage**: re-run just the failing spec (optionally `--headed`), read the failing step
in the report (`frontend/playwright-report/`), and include the report path when summarizing.
- A timeout on the first test of a run usually means the backend never became healthy — check
that `venv/bin/uvicorn` exists and imports cleanly (`venv/bin/python -c "import sys; sys.path.insert(0,'src'); import main"`).
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ artifacts/

# Local environment and tooling
.env
.claude/
.claude/settings.local.json

# Ad-hoc bespoke generator scripts kept locally as reference material (not canonical)
scripts/reference/

# Generated worksheet series output (produced by scripts/generate_*.py)
*_series/
Expand Down
65 changes: 65 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# CLAUDE.md

Homeschool curriculum app: FastAPI backend (`src/`) + Next.js frontend (`frontend/`) + offline
worksheet generators (`scripts/`). SQLite DB at `curriculum.db`.

## Environment bootstrap

Always use `venv/bin/python` — never system python. The E2E suite hard-codes `venv/bin/uvicorn`
(`frontend/global-setup.ts`), so the venv must live at the repo root.

```bash
# Python 3.11–3.13 required (CI uses 3.12; Pillow<11 does not build on 3.14+).
# If system python is too new, use uv:
uv venv --python 3.12 --seed venv
venv/bin/pip install -r requirements.txt

cd frontend && npm install && npx playwright install chromium
venv/bin/python src/ingest_standards.py # creates curriculum.db (one-time)
```

## Commands

| What | Command | Notes |
|------|---------|-------|
| Backend dev server | `cd src && ../venv/bin/uvicorn main:app --reload` | port 8000 |
| Frontend dev server | `cd frontend && npm run dev` | port 3000 |
| Backend tests | `venv/bin/python -m pytest tests/ -v --tb=short` | fast (~3s) |
| E2E tests | `cd frontend && npm run test:e2e` | self-contained, see below |
| Type check | `cd frontend && npm run type-check` | |
| Format check | `cd frontend && npm run format:check` | Prettier |
| Python lint/format | ruff + black, 100-char lines | pre-commit enforces |
| Generate a week packet | `venv/bin/python scripts/generate_<theme>_week_series.py` | see /generate-week skill |

The E2E suite spawns its own backend on port 8182 with an isolated DB (`/tmp/playwright-test.db`)
and a frontend test server on port 3002 — it never touches `curriculum.db` or the dev servers.

## Architecture map

- `src/main.py` — FastAPI app; students, weekly packets, feedback, artifacts endpoints
- `src/agent.py` — OpenAI-driven weekly plan generation (needs `OPENAI_API_KEY`)
- `src/trio_generator.py` — generates 3 plans per student post-feedback; NTFY notify via `src/ntfy.py`
- `src/worksheet_html_renderer.py` — HTML print-packet engine (preferred for printables)
- `src/worksheet_renderer.py` — PIL engine (PNG/PDF, image-heavy types)
- `src/worksheets/` — 20 worksheet types; `factory.py` is the unified entry point
- `tests/` — pytest; fixtures in `conftest.py`, builders in `factories.py` (temp DBs, never `curriculum.db`)
- `frontend/e2e/` — Playwright specs; seeding helpers in `fixtures/api.ts` (backed by `scripts/e2e_seed.py`)

## Conventions

- **Read `AGENTS.md` before any worksheet/lesson work** — worksheet-type table, pedagogy patterns,
renderer selection, and git workflow rules live there. Do not duplicate them here.
- **Never commit to `main`** — branch from `origin/main`, open a PR (AGENTS.md §7).
- Conventional commits: `feat:` / `fix:` / `docs:`.
- **Week output rule**: a generated week's assets go in its own `<theme>_week_series/` directory at
the repo root — `<theme>_week.html` (student packet) and `<theme>_week_teacher_guide.html`
(teacher guide), both emitted by the generator script. Output dirs are gitignored: the script is
the only tracked artifact and must fully reproduce the week.
- `scripts/reference/` (gitignored, may be absent) holds bespoke one-off generator scripts kept as
local reference — not canonical patterns.

## Skills

- `/feature <request>` — full pipeline: explore → plan → implement on a branch → tests → PR
- `/generate-week <theme>` — author a themed 5-day printable week offline (no OpenAI key)
- `/test` — run/interpret the pytest and Playwright suites, including single-test invocations
4 changes: 4 additions & 0 deletions frontend/.prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@ coverage
# Cache
.cache
.eslintcache

# Playwright artifacts
test-results
playwright-report
Loading
Loading