# Graspful

Adaptive learning platform. Multi-tenant white-label SaaS.

## Critical Rules

- **CLI or MCP for everything.** Browser auth is only used to mint credentials for the CLI; course creation and publishing stay programmatic.
- **Use `bun` not `npm`.**
- **Read `docs/adding-a-course.md` before building any course.** It is the canonical agent runbook.
- **Auth, routing, and entitlement changes require regression proof.** Verify anonymous access, signed-in but non-entitled access, and entitled access, and cover shell/chrome expectations on the affected route with automated tests. For user-facing access control changes, run the relevant e2e path before considering the work done.

## Course Creation , Agent Workflow

> If a user asks you to build, draft, author, or create a course, follow this section. Do NOT improvise a workflow , use the CLI/MCP tools below.

### Authentication , Read This First

**You can scaffold, fill, validate, and review courses without any account.** These operations run locally.

**You MUST authenticate before importing or publishing.** Five tools require auth: `graspful_import_academy`, `graspful_import_course`, `graspful_publish_course`, `graspful_import_brand`, `graspful_list_courses`.

To authenticate, do ONE of:
1. **CLI (recommended for first-time setup):** Run `graspful register [--email <email>]` , this opens browser auth, then saves an API key to `~/.graspful/credentials.json`.
2. **Environment variable:** Set `GRASPFUL_API_KEY=gsk_...` before starting the MCP server.

If you try an authenticated tool without auth, you'll get a clear error telling you to register first.

### Step 1: Install the CLI

```bash
bunx @graspful/cli init
```

This auto-detects your editor (Claude Code, Cursor, Windsurf, Codex) and configures MCP if supported. No global install needed.

If you have MCP configured, skip the CLI and use MCP tools directly (see Step 1b).

### Step 1b: MCP alternative

If MCP is already configured, you have these tools available , no CLI needed:

| Tool | Auth? | Description |
|------|:---:|-------------|
| `graspful_create_academy` | No | Generate academy plan and manifest YAML |
| `graspful_scaffold_course` | No | Generate course YAML skeleton |
| `graspful_fill_concept` | No | Add KPs and problems to a concept |
| `graspful_validate` | No | Validate YAML against schema |
| `graspful_review_course` | No | Run 10 quality checks, including teaching alignment |
| `graspful_describe_course` | No | Course statistics |
| `graspful_create_brand` | No | Generate brand YAML (required , every org needs a brand) |
| `graspful_import_academy` | **Yes** | Import academy manifest + course YAMLs (saved CLI credentials or `GRASPFUL_API_KEY`) |
| `graspful_import_course` | **Yes** | Import course to platform (saved CLI credentials or `GRASPFUL_API_KEY`) |
| `graspful_publish_course` | **Yes** | Publish a draft course (saved CLI credentials or `GRASPFUL_API_KEY`) |
| `graspful_import_brand` | **Yes** | Import brand config , required for site to work (saved CLI credentials or `GRASPFUL_API_KEY`) |
| `graspful_list_courses` | **Yes** | List org courses (saved CLI credentials or `GRASPFUL_API_KEY`) |

Tools marked "No" for auth work offline , no account needed. Tools marked **Yes** will fail with a clear error if you haven't authenticated. Run `graspful register`. MCP reads the saved CLI credentials when a tool runs; `GRASPFUL_API_KEY` can override them.

**MCP discovery:** To check if MCP is active, try calling `graspful_validate` with any small YAML string. If it responds, MCP is working. If you get a "tool not found" error, fall back to the CLI.

### Step 2: Register (before importing/publishing)

If you haven't already authenticated (see "Authentication" above), do it now before proceeding to import.

**CLI:**
```bash
graspful register --email <email>
```

This creates an account, private org workspace, and API key through browser auth. MCP can use the saved CLI credentials. Registration does not create a public website. See [auth and access](docs/auth-and-access.md).

### Step 3: Build a course

The workflow is: academy plan -> course graphs -> fill -> validate -> review -> import.

**Before writing any YAML**, follow the detailed runbook in `docs/adding-a-course.md`. Key steps:
1. Gather source material (official docs, syllabi, PDFs , not marketing copy)
2. Model it as an academy first, even if it starts with one course
3. Decompose the academy into foundations -> structures -> operations -> applied judgment
4. Build the prerequisite graph (roots -> trunk -> branches -> leaves)
5. Write the YAML skeleton (graph first, content second)
6. Spend a cycle on the academy landing page so the promise and proof are specific to the learner
7. Fill concepts one at a time
8. Validate and review

```bash
# 1. Scaffold the academy plan
graspful create academy --topic "Your Topic" -o academy.yaml

# 2. Edit the academy plan until source material, learner promise,
# landing-page proof, and course dependencies are specific.

# 3. Scaffold each course knowledge graph
graspful create course --topic "Your Topic" --hours 10 -o course.yaml

# 4. Fill each concept with knowledge points and problems
graspful fill concept course.yaml <concept-id>

# 5. Validate after every edit
graspful validate course.yaml

# 6. Review must score 10/10 to publish
graspful review course.yaml

# 7. Import and publish
graspful import academy.yaml --org <org-slug> --course-dir .
```

Or with MCP tools:

```
graspful_create_academy(topic: "Your Topic")
graspful_scaffold_course(topic: "Your Topic", estimatedHours: 10)
-> edit the YAML
graspful_fill_concept(yaml: "...", conceptId: "concept-id")
graspful_validate(yaml: "...")
graspful_review_course(yaml: "...")
graspful_import_academy(manifestYaml: "...", courseYamls: { "courses/course.yaml": "..." }, org: "org-slug", publish: true)
```

### Images, Videos, Links in Course Content

Course YAML supports rich media through **content blocks** on two fields: `instructionContent` and `workedExampleContent`. Each is an array of typed blocks.

**Image block:**
```yaml
instructionContent:
  - type: image
    url: https://example.com/photo.jpg
    alt: Description for accessibility
    caption: Optional caption text
    width: 960  # optional, positive integer
```

**Video block:**
```yaml
instructionContent:
  - type: video
    url: https://youtube.com/watch?v=abc123
    title: Video title
    caption: Optional caption
```

**Link block:**
```yaml
instructionContent:
  - type: link
    url: https://example.com/reference
    title: Link title
    description: Optional description
```

**Callout block:**
```yaml
instructionContent:
  - type: callout
    title: Important distinction
    body: The explanation text here.
```

**Important:** `instruction` and `workedExample` (the plain text fields) should remain readable as standalone text because they power audio. Put images, diagrams, external references, and video links in the `*Content` blocks , do not bury URLs in the prose.

When a user asks for images or visual comparisons in a course, use `image` content blocks with publicly accessible URLs. Every knowledge point can have multiple content blocks.

### Working from source material (PDFs, documents, etc.)

When building a course from a PDF or document:

1. Read the full source material first
2. Extract the key concepts, facts, and distinctions
3. Map them to a prerequisite graph (what must be learned before what?)
4. For visual content (photos, diagrams, comparisons), find or request publicly accessible image URLs and use `image` content blocks
5. Do not copy-paste prose verbatim , rewrite for the lesson pattern (instruction -> worked example -> problems)

### Step 4: Create or update the academy landing page and brand

A public website needs a brand. Registration creates a private org workspace. A course import can create a public brand, including when the imported course is a draft. Customize its content for the course topic.

Use `graspful_create_brand` to generate a brand YAML tailored to the course topic, 
then import it with `graspful_import_brand`. This updates the landing page headline, 
features, and SEO to match the actual course content.

Important:

- do not leave the landing page as generic niche filler
- the page should name the learner, the outcome, the curriculum shape, and the proof for why this academy is worth joining
- treat landing-page authoring as part of the academy build, not post-launch polish

If the org already has a brand with the same slug, importing it updates that brand in place.

Create a white-label landing page and theme:

```bash
graspful create brand --niche tech --topic "Your Topic" --name "My Academy" --org my-org -o brand.yaml
graspful import brand.yaml
```

**Brand theme requirements:** When creating a brand with custom `light`/`dark` theme colors (not using a preset), you **must** also include:
- A `gradient` object with hex colors: `start`, `mid`, `end`, `accent` , used for CTA buttons, text gradients, and decorative orbs on marketing pages.
- All 18 color fields in both `light` and `dark`: `primary`, `primaryForeground`, `secondary`, `secondaryForeground`, `accent`, `accentForeground`, `background`, `foreground`, `card`, `cardForeground`, `popover`, `popoverForeground`, `muted`, `mutedForeground`, `destructive`, `border`, `input`, `ring`.

Missing fields will fall back to defaults, but providing them all ensures visual consistency. **After importing a brand, run the brand resilience tests:**

```bash
cd apps/web && bunx vitest run src/lib/brand/__tests__/resolve-db.test.ts src/__tests__/brand/theme-style.test.tsx
```

### Key rules

- **CLI or MCP for everything.** Browser auth is only for minting credentials; course creation, import, and publishing stay programmatic.
- **Validate after every edit.** It's offline and fast.
- **Fill one concept at a time.** Quality is better than filling the whole course at once.
- **Review before import.** The server rejects courses that fail the quality gate.
- **Problem IDs must be globally unique.** Use `{concept-id}-{kp-index}-p{problem-index}`.

### Output format

Pass `--format json` to any CLI command for machine-readable output:

```bash
graspful validate course.yaml --format json
```

### Environment variables

| Variable | Description |
|----------|-------------|
| `GRASPFUL_API_KEY` | API key for import/publish (set automatically by `register` or `login`) |
| `GRASPFUL_API_URL` | API base URL (default: `https://api.graspful.ai`) |

## Tech Stack

- **Frontend:** Next.js 16 (App Router), React, Tailwind CSS, shadcn/ui
- **Backend:** NestJS (TypeScript), Prisma ORM, PostgreSQL (Supabase-hosted)
- **Auth:** Supabase Auth (JWT)
- **Monorepo:** Turborepo, bun as package manager

## Architecture , DDD Bounded Contexts

The backend follows Domain-Driven Design with bounded contexts. Each NestJS module owns its domain. **Do not leak domain logic across boundaries.**

| Context | Module | Aggregate Root | Owns |
|---------|--------|---------------|------|
| Knowledge Graph | `knowledge-graph/` | Course | Concepts, KnowledgePoints, PrerequisiteEdges, EncompassingEdges |
| Student Model | `student-model/` | StudentProfile | ConceptState, KPState, mastery, enrollment |
| Diagnostic | `diagnostic/` | DiagnosticSession | BKT engine, MEPE selector, stopping criteria, session persistence |
| Learning Engine | `learning-engine/` | LearningSession | Task selection, frontier, mastery enforcement, remediation |
| Assessment | `assessment/` | Assessment | Problems, answer evaluation, reviews, quizzes |
| Spaced Repetition | `spaced-repetition/` | RepetitionSchedule | FIRe algorithm, review scheduling |
| Gamification | `gamification/` | PlayerProgress | XP, streaks, leaderboards |

### DDD Rules

1. **Services call services, not repositories of other modules.** If Diagnostic needs mastery data, it calls `StudentStateService`, not `prisma.studentConceptState` directly.
2. **Controllers are thin.** Extract, validate, delegate to service, return. No domain logic in controllers.
3. **Each module owns its Prisma queries.** Other modules request data through the owning module's service.
4. **Cross-context data for the frontend** should be composed at the API/controller layer or in a dedicated query service , not by having one domain service reach into another's tables.

## Backend

- Build: `cd backend && bun run build` (generates Prisma client, then compiles NestJS)
- Run: `TS_NODE_PROJECT=tsconfig.runtime.json node -r tsconfig-paths/register dist/main.js`
- Dev: `bun run dev` (nest start --watch)
- Test: `bun run test`
- Port: 3000

## Frontend

- Dev: `bun run dev` (port 3001)
- Build: `bunx next build`
- E2E: `cd apps/web && bunx playwright test`

## Prisma

- Schema: `backend/prisma/schema.prisma`
- Migrate: `cd backend && bunx prisma migrate dev --name <name>`
- Generate: `bunx prisma generate` (runs automatically after migrate)

## Conventions

- Use `bun` not `npm`
- snake_case for DB columns (Prisma `@@map`), camelCase for TypeScript
- All Prisma models use `@db.Uuid` for IDs and `@db.Timestamptz` for dates
- Tests: Jest for backend unit tests, Vitest for app component tests, `bun test` for package and script tests, Playwright for e2e
- Checks: `bun run lint`, `bun run typecheck`, and `bun run test:scripts` from the root
- Local setup and browser tests: [docs/local-e2e.md](docs/local-e2e.md)
- E2E helpers: `apps/web/e2e/helpers/auth.ts` , `signUpTestUser()` creates fresh users
- Brand cookie: `dev-brand-override` selects org in dev

## E2E Test Coverage Requirements

**Every live site page and API endpoint MUST have an e2e test.** This is non-negotiable , bread-and-butter functionality that users depend on must have regression coverage.

### What must be tested:

1. **All pages render** , every route under `(marketing)` and `(app)` must have a smoke test verifying it returns 200 and renders its heading. See `e2e/docs-smoke.spec.ts` for the pattern.
2. **Auth flows** , sign-up, sign-in, sign-out, email confirmation callback, org provisioning
3. **Creator flows** , API key CRUD, course import, brand config import, course publish
4. **CLI registration** , `graspful register` opens browser auth, then stores an API key locally for later CLI/MCP use
5. **API provisioning** , `POST /auth/provision` creates personal org for web UI sign-ups
6. **Learner flows** , browse, enroll, diagnostic, study session

### When adding a new page or endpoint:

- Add an e2e test in the same PR
- If it's a new doc page, add it to the `DOCS_PAGES` array in `e2e/docs-smoke.spec.ts`
- If it's a new API endpoint, add API-level tests using the `helpers/api-auth.ts` helpers

## Links

- Documentation: https://graspful.ai/docs
- CLI reference: https://graspful.ai/docs/cli
- MCP setup: https://graspful.ai/docs/mcp
- Course YAML schema: https://graspful.ai/docs/course-schema
- Brand YAML schema: https://graspful.ai/docs/brand-schema
- Full docs for LLMs: https://graspful.ai/llms-full.txt
