The engineering guide every contributor reads before their first change. It states the project model in one breath (a frontend that implements no legal- AI logic, talking to lq-ai only through its published API), the cardinal rules (never edit vendor/lq-ai; consume the generated contract; keep the bar green; merge, never squash; evidence before claims), the BFF architecture, the repo layout, the superpowers build loop, the conventions to match, the upstream-request workflow, and the hard-won gotchas. Names the current backend pin (c4d4482) and points at where the deep history lives. Before your first change — the map of how to build in Donna well.
The superpowers build loop
Click any node to read its source — a doc section or the code itself.
The upstream-request workflow
Click any node to read its source — a doc section or the code itself.
CLAUDE.md — engineering guide for Donna
This file orients a coding co-pilot — a human developer or another AI agent (Claude Code reads it automatically on session start) — so you can understand the whole project and pick up the roadmap from where it stands. Read it once, top to bottom, before your first change.
For what the product is and why, read docs/PRODUCT.md first. For how to run it, README.md. This file is how to build in it well.
1. What Donna is, in one breath
Donna is a standalone SvelteKit app — a friendly, document-forward frontend for the LQ.AI legal-AI backend. Donna implements no legal-AI logic itself: retrieval, the citation engine, anonymization, skills, playbooks, tabular review, and the autonomous runtime all live in lq-ai. Donna's job is to make that power usable, transparent, and controllable through a clean reading-first UI.
Donna talks to lq-ai only through its published API, and vendors that backend as a pinned
git submodule (vendor/lq-ai) so the whole product runs from one compose file.
2. The cardinal rules (violate these and you'll break the project's model)
- Never edit
vendor/lq-ai. It is a pinned submodule, not our code. If you need backend behavior that doesn't exist, that's an upstream request (§8), not a local patch. - Consume the contract; never hand-fork it. API types are generated from lq-ai's OpenAPI into
src/lib/api/backend.d.ts via
npm run gen:api. Derive types from there. (Where the backend types a field loosely —additionalProperties— hand-type it in a small parser and say so in a comment; see theparseTabularResults/parseFindingListprecedents.) - The bar is green, not "no worse."
npm run check= 0 errors / 0 warnings;npm run lint= prettier + eslint fully clean; the unit suite passes. Keep it that way. - Merge PRs with a MERGE COMMIT. A squash would orphan the two one-time-format SHAs in
.git-blame-ignore-revs. Never squash tomain. - Evidence before claims. "It works" requires a run you can point to — a passing test, a live e2e, an actual page load. Report failures faithfully.
3. Architecture — the backend-for-frontend (BFF)
The browser talks only to Donna's SvelteKit server. That server:
- Holds the lq-ai JWT access + refresh tokens in httpOnly cookies (never exposed to client JS).
- Attaches
Authorization: Bearerwhen proxying to the lq-aiapi, and transparently refreshes on401. No CORS anywhere. - Is the single trust boundary: auth lives in src/hooks.server.ts + src/lib/server/.
Consequences you must internalize:
- Server-only code (cookies, the authed
lqClient, auth wrappers) lives under src/lib/server/. Never import it into client code. - A page gets backend data through a SvelteKit
load(SSR) and mutates through form actions or small BFF proxy routes (+server.ts) — not by calling lq-ai from the browser. - Proxy routes exist to (a) attach auth and (b) avoid page/endpoint route collisions — e.g.
/prompts/itemssits beside the/promptspage, and the/tabular-executions/[id]proxy is a separate top-level group precisely so it doesn't collide with the/tabular/[executionId]page.
4. Repo layout
src/ SvelteKit app
routes/(app)/ authed app routes (the product)
routes/(auth)/ login / change-password (guarded by hooks.server.ts)
lib/ feature modules — one dir per domain
server/ SERVER-ONLY: session cookies, authed lqClient, auth wrappers
api/ generated OpenAPI types (npm run gen:api) — do not hand-edit
docpanel/ document panel: PDF render, highlight, TextViewer (md/plain)
automations/ autonomous runs: sessions, receipts, results, schedules, watches
tabular/ playbooks/ skills/ prompts/ matters/ knowledge/ inference/ …
hooks.server.ts auth routing / token refresh (the global guard)
vendor/lq-ai/ pinned lq-ai backend (submodule) — NEVER edit
docs/ see docs/README.md for the full index
tests/ Playwright e2e (live, against the running stack)
static/learn/ interactive playgrounds served by the /about guide
5. Dev stack — run & verify
Prereqs: Docker + Compose v2, Node 22+. Full setup in the README; the essentials:
# cold start (loads .env, builds, brings up the explicit service list)
set -a; . ./.env; set +a
docker compose up -d --build postgres redis minio gateway api donna-web ingest-worker arq-worker
- App at http://localhost:13002, lq-ai api at http://localhost:18000 (ports are shifted
in
.envso Donna can coexist with a separate raw lq-ai dev stack on the defaults). apiis the single schema migrator (workers run withLQ_AI_SKIP_MIGRATIONS=1). After a pin bump, rebuildapi+arq-worker+ingest-workertogether so siblings don't crash-loop on a revision mismatch.ingest-workerpowers ingestion/RAG + data export;arq-workerpowers tabular runs, playbook generation, and automations. Thearq-workermounts./skills:/skills:roand exits at startup if the skills dir is missing — which is why the clone must be--recurse-submodules(the skills corpus is a nested submodule).- First-run admin fixture for login/e2e:
docker compose exec api python -m app.cli reset-admin-password --email admin@lq.ai --password '<pw>' --no-force-change
Gates (run before claiming done):
npm run check # svelte-check — must be 0 errors / 0 warnings
npm run lint # prettier + eslint — must be fully green
npx vitest run # unit/component — keep the suite passing
npx playwright test # live e2e — needs the stack up + the admin fixture
npm run checkprints a harmlessERR_MODULE_NOT_FOUNDreferencingvendor/lq-ai/...; svelte-check recovers and still reports0 ERRORS. The vendored backend is excluded from check/lint. Rebuilddonna-web(docker compose up -d --build donna-web) before any manual or e2e check — the running container serves built code, not your working tree.
6. The build workflow (how every feature here was shipped)
Donna is built with the superpowers skill loop. For any non-trivial change, follow it:
- Brainstorm (
superpowers:brainstorming) → a design doc indocs/superpowers/specs/. - Plan (
superpowers:writing-plans) → a task-by-task plan indocs/superpowers/plans/. - Execute (
superpowers:subagent-driven-development) — a fresh implementer subagent per task, each doing TDD (failing test → implement → green → commit), followed by two-stage review (spec compliance, then code quality) per task. - Whole-branch review (Opus) before the PR.
- PR with a merge commit. Commit + push per task as you go.
Scale the ceremony to the change: a one-line fix doesn't need a spec, but anything that adds a
surface or touches a contract does. The specs/plans for every shipped phase are archived under
docs/superpowers/ — read the closest analog before building (e.g. mirror how Playbooks did
list→detail→execute→poll→typed results, or how Automations threaded memories_total through the
receipt chain).
7. Conventions & patterns (match these)
- Svelte 5 runes throughout (
$props,$state,$derived,$effect). Seed reactive controllers fromdataonce viauntrack(() => …)to avoidstate_referenced_locallywarnings. - Tabs for indentation (prettier-enforced) — copy a neighboring file's style.
- Defensive parsers at the data boundary: a
parseXList(raw: unknown)with localstr/objguards that drops malformed rows rather than throwing. (findings.ts,artifacts.ts,schedules.tsare the templates.) - Honest degradation: a server loader degrades each sub-fetch to
nullindependently — a failed Results fetch hides a section or shows "unavailable"; it never breaks the page or fabricates data. Live pollers keep last-known-good (only overwrite state on non-null incoming). - Form-action server tests: mock
lqFetch, build aRequestwith aURLSearchParamsbody, assert the redirect/fail. (See any+page.server.test.tsunderroutes/(app)/.) - Live e2e are real Playwright runs against the stack, self-cleaning (try/finally teardown).
When a feature's output is model-discretionary (artifacts, findings), SQL-seed marker rows via
docker compose exec -T postgres psql(credslq_ai/lq_ai); helpers live in tests/automations-memory-review.spec.ts / tests/automations-artifacts.spec.ts. Postgres note:fileskeys onowner_id; autonomous tables key on the parentsession_id. - Doc panel: open a document with
docPanel.open({ source_file_id, … } as Citation); the panel routes PDFs toPdfViewer,text/markdown/text/plaintoTextViewer, everything else toUnsupportedFileCard.
8. The upstream-request workflow (when the API isn't enough)
You will hit gaps where lq-ai doesn't expose what a feature needs. Do not work around it by editing the submodule or coupling to internals. Instead:
- Write the ask to
docs/upstream-requests/lq-ai-<short-name>.md— the gap, the proposed contract, and why. Use absolute file paths if the lq-ai maintainer works in a different checkout. - The human relays it to the lq-ai maintainer session.
- When the fix merges, bump the pin:
Then verify the merged contract in src/lib/api/backend.d.ts — the merged shape wins over the ask — record the bump in docs/decisions/lq-ai-pin.md, and build the consuming slice.cd vendor/lq-ai && git fetch && git checkout <sha> cd ../.. && npm run gen:api # regenerate types docker compose up -d --build api arq-worker ingest-worker donna-web # migrations run on api boot
docs/decisions/lq-ai-pin.md is the running log of every pin bump and what it unblocked — read its
top entry to know what backend you're on (currently c4d4482).
9. Picking up a roadmap item
- Read docs/PRODUCT.md (the why) and docs/roadmap/donna-future-roadmap.md (what's deferred and why, with pickup context).
- Check whether it's buildable now or upstream-blocked. If blocked, the roadmap names the upstream request; that path is §8.
npm run gen:apiand confirm the contract supports it (types over assumptions).- Find the closest shipped analog in
docs/superpowers/and mirror its shape. - Run the loop in §6. Keep the gates green. Open a PR with a merge commit.
10. Hard-won gotchas (don't relearn these)
.txtwon't ingest (unsupported_type) — use.pdffixtures for ingestion/RAG e2e.skill_inputsonly interpolate{{placeholder}}tokens in a skill body; the gateway appends unreferenced bound inputs as a labelled context block (lq-ai #115) so they still reach the model.- The SSE complete frame echoes
applied_*fields at the top level (e.g.applied_skills,applied_file_ids,routed_inference_tier). - Chat citations come from a per-message endpoint, not the SSE complete frame.
- Autonomous run artifacts are markdown/plain-text in v0.1.0; the doc panel renders them inline. PDF/DOCX artifact rendering is an open upstream item (DE-332).
- A SvelteKit form-action POST needs an
Originheader (CSRF) — browsers and Playwright send it; rawcurldoesn't, so a hand-forgedcurllogin will 403/404. Test login via Playwright. - Production builds set
Securesession cookies → any non-localhost deploy must terminate TLS in front ofdonna-webor login silently fails.
11. Where the deep history lives
docs/superpowers/specs/+plans/— the design + task breakdown for every shipped phase.docs/superpowers/HANDOFF-*.md— session handoffs (point-in-time; the pin log + this file are the durable references).- docs/decisions/lq-ai-pin.md — every backend pin bump and what it unblocked.
docs/upstream-requests/— the asks filed to lq-ai (some resolved, some open).- The app itself — sign in and open
/aboutfor the richest, most current explanation of every feature, including playgrounds for the LQ-AI engine.