- Python 3.12 (e.g. via uv)
- Node.js 20+ and npm
No global Python packages are needed — the spaCy model is a pinned pip dependency.
# Backend
cd backend
python3.12 -m venv .venv # or: uv venv --python 3.12 .venv
source .venv/bin/activate
pip install -e ".[dev]" # or: uv pip install -e ".[dev]"
# Frontend
cd ../frontend
npm install # or: npm ciOptional local embeddings for semantic validation:
cd backend && pip install -e ".[semantic]"The model loads lazily on first use and is never downloaded at startup. Without
it, suggestions still work and meaning checks report UNAVAILABLE.
# Backend on :8000
cd backend && uvicorn app.main:app --reload --port 8000
# Frontend on :5173 (proxies /api to :8000)
cd frontend && npm run devcd backend && .venv/bin/pytest -q # 675 tests, deterministic, no network
cd frontend && npm run typecheck # tsc --noEmit
cd frontend && npm test # 195 tests across 13 files (Vitest + RTL)
cd frontend && npm run build # typecheck + production buildAll tests must be deterministic and offline:
- Provider behaviour is mocked with
httpx.MockTransportor replaced by the deterministic fake provider. - Semantic tests use
FakeEmbeddingBackend. They never download or load a real model. - Frontend tests are jsdom only and never touch the network.
- Tests read no developer
.env:get_suggestion_serviceandget_semantic_validatorarelru_cached, so tests override the FastAPI dependency or set environment variables explicitly.conftest.pydisables semantic validation by default.
New metrics need golden tests with manually verifiable expected values. A failing test is fixed in the implementation, never weakened — unless the test itself is wrong, which should be rare and explained.
- Production-quality, typed, modular code. Files stay focused; avoid inventing tiny abstractions for their own sake.
- Validate all API inputs. Never return
NaNorInfinity. - Compute each metric once and share the value — no duplicate logic.
- Do not add dependencies without justification. There is deliberately no NumPy, no charting library and no LLM SDK.
- Routes stay thin; logic belongs in a service.
- All tunable constants live in
app/config.py.
src/styles.cssis the single source of design tokens. Never scatter hex values or one-off styles into components — reuse tokens and the shared classes (.btn,.badge,.notice,.kv-list,.status-line,.empty-state).- Light theme only.
- Button hierarchy:
.btn-primaryfor Analyze / Suggest alternatives / Apply,.btn-secondary,.btn-ghost, and.btn-outline-danger/.btn-dangerfor destructive or override actions. An override must never look ordinary. - Destructive and override actions use the
ConfirmDialogprimitive (Escape cancels, focus moves inside, Tab is trapped, backdrop click cancels). - Statuses are conveyed by text plus shape or icon, never colour alone.
- Every user-facing string uses stable terminology and contains no sprint labels, internal enum names or debug identifiers.
EmptyStatefor empty states, spinners withrole="status"for loading,.noticefor informational copy. Never fake a progress percentage.- Responsive: side-by-side editor above 1120px, stacked below; the problem map
stacks at 900px; mobile stack at 720px. Guard grids with
min-width: 0and useoverflow-wrapto prevent horizontal overflow. prefers-reduced-motionmust keep working.- Frontend tests query by role or text, never by CSS class, and use no page snapshots.
These have each cost time at least once.
- spaCy abbreviations — the sentencizer treats a single letter plus a period
(
"t.") as an abbreviation. Tests that need many sentences must use multi-letter words. - Emoji and astral characters tokenize as word tokens in spaCy and therefore count toward word counts.
- Offsets are UTF-16 code units, matching JavaScript string indexing, not
Python code points. Issue
excerptfields come verbatim from the original text and the frontend relies on those offsets for marks. - Editor offsets are valid only against the analysis snapshot. The UI marks the map stale after any edit and disables stale navigation until re-analysis. This is by design, not a bug to fix.
- Suggestion freshness — the backend re-slices the supplied text at the
issue's UTF-16 offsets and rejects mismatches as
STALE_ANALYSIS(409). .venvlives inbackend/and is never committed. The spaCy model is a pinned pip dependency rather than a post-install download.- Node and Python tooling may be installed user-locally in
~/.local/bin; make sure it is onPATH. - Readability uses textstat's internal tokenization while everything else uses spaCy. This is documented and intentional.
- Semantic pipeline uses its own lazy spaCy pipeline with parser and NER
disabled. The tagger needs
tok2vec, so only parser and NER can be disabled. - Legacy profiles (no
advancedkey) derive only what their pooled fields support. Opening repetition is never derived. - Custom protected terms are literal and case-sensitive substring matches, not regexes or fuzzy matches.
- Do not modify unrelated files, and do not scan
.venv,node_modules,dist,build,__pycache__or.git. - Use targeted commands rather than large log dumps.
backend/app/ FastAPI application, all local analysis and validation
backend/tests/ 675 deterministic tests
frontend/src/api/ typed API client
frontend/src/editor/ editing workspace, undo/redo, diff, suggestion hooks
frontend/src/components/ one component per screen or analysis section
docs/ this documentation set