Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6e354ae
docs(plans): commit the reconciled phase 5 plan
rsml Jul 23, 2026
ce4aabf
refactor(server): name the last two delays, trim provenance comments,…
rsml Jul 23, 2026
69be914
docs(readme): add badges, architecture overview, and a refreshed dev …
rsml Jul 23, 2026
8f253b2
fix(library): make book cards keyboard operable
rsml Jul 23, 2026
19d8727
fix(reader): hide the redundant section tap zones from assistive tech
rsml Jul 23, 2026
e166124
fix(reader): associate feedback form labels with their textareas
rsml Jul 23, 2026
5f8ea74
fix(library): wire the rename dialog labels to their inputs
rsml Jul 23, 2026
0caa861
fix(library): label the delete dialog confirmation input
rsml Jul 23, 2026
176b238
fix(library): add aria-label to the toolbar's icon-only controls
rsml Jul 23, 2026
a64e496
chore(scripts): adopt the AST survey for icon-only unnamed buttons
rsml Jul 23, 2026
a1eeae2
refactor(client): dedupe ProfileResponse from shared/responses
rsml Jul 23, 2026
9b2f973
fix(server): add an exhaustiveness guard to the provider switch
rsml Jul 23, 2026
51b6924
refactor(adapters): remove a no-op reassignment in saveReference
rsml Jul 23, 2026
b4586d7
chore(scripts): add the fresh clone verification gate
rsml Jul 23, 2026
40e7fbc
chore: add knip and remove the dead code, deps, and exports it found
rsml Jul 23, 2026
09607da
fix: repoint diagnose-quiz.ts at the post-hexagonal quiz service
rsml Jul 23, 2026
dc31416
feat: add scripts/check-doc-paths.ts
rsml Jul 23, 2026
ce046f2
chore: delete the dead ralph.sh loop runner
rsml Jul 23, 2026
81f5686
chore: prune two unreferenced screenshots
rsml Jul 23, 2026
5070625
ci: run knip and the doc-paths checker in the verify job
rsml Jul 23, 2026
7332b1f
docs: add the other half of the flake policy to e2e/README.md
rsml Jul 23, 2026
24cf847
chore: take knip's own config hints and cover mts scripts
rsml Jul 23, 2026
6157153
chore: mirror the new knip and doc-path gates into pre-push
rsml Jul 23, 2026
2929a63
docs(readme): annotate the top level tree for cold readers
rsml Jul 23, 2026
81827e5
test(domain): pin the two pure functions that had no colocated test
rsml Jul 23, 2026
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
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,21 @@ jobs:

- run: pnpm test

- run: pnpm knip

- run: pnpm tsx scripts/check-doc-paths.ts

# scripts/find-unnamed-buttons.mts lands via a sibling task. Guarded so
# this step is a no-op until that branch merges, rather than failing
# every run in the meantime.
- name: Check for unnamed accessible buttons (client)
run: |
if [ -f scripts/find-unnamed-buttons.mts ]; then
pnpm tsx scripts/find-unnamed-buttons.mts client
else
echo "scripts/find-unnamed-buttons.mts not present yet (lands via a sibling task) — skipping."
fi

# docs/api-routes.md is generated from the Fastify route registry, so it
# can only be wrong if someone changed a route and did not regenerate it.
# This step is what makes that impossible to merge. It runs last because
Expand Down
39 changes: 36 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
# Tutor

[![CI](https://img.shields.io/github/actions/workflow/status/rsml/tutor/ci.yml?branch=master&label=CI)](https://github.com/rsml/tutor/actions/workflows/ci.yml)
[![License: GPL-3.0](https://img.shields.io/github/license/rsml/tutor)](LICENSE)
[![Latest Release](https://img.shields.io/github/v/release/rsml/tutor)](https://github.com/rsml/tutor/releases/latest)

Start here. [ARCHITECTURE.md](ARCHITECTURE.md) is the entry point for technical readers. The vocabulary is in [CONTEXT.md](CONTEXT.md) and the decisions, with what each one cost, are in [docs/adr/](docs/adr/README.md).

**Read smarter — personal tutors disguised as books.**

Books suggested and generated just for you based on your feedback, quiz results, and unique learning style.

Talks about this project live in [rsml/talks](https://github.com/rsml/talks).

Reading the code? Start at [ARCHITECTURE.md](ARCHITECTURE.md). The vocabulary is in [CONTEXT.md](CONTEXT.md) and the decisions, with what each one cost, are in [docs/adr/](docs/adr/README.md).

<p align="center">
<img src="docs/screenshots/library.png" alt="Tutor library showing AI-generated books with custom covers" width="100%">
</p>
Expand Down Expand Up @@ -98,6 +102,34 @@ Give feedback on each chapter. The next one adapts to your quiz results and lear
<img src="docs/screenshots/settings.png" alt="Learning profile and settings" width="100%">
</p>

## Architecture

```mermaid
flowchart LR
features["client/features/<br/>feature slices"] --> api["client/api/<br/>typed API client"]
api -->|"HTTP and SSE"| routes["server/routes/<br/>thin routes"]
routes --> services["server/services/"]
services --> ports["server/ports/"]
ports --> adapters["server/adapters/"]
```

Client feature slices call the server through one typed API client. Routes stay thin and hand off to services, which depend on ports rather than concrete adapters.

See [ARCHITECTURE.md](ARCHITECTURE.md) for the full five-diagram hub, including how the pieces talk to each other, the server hexagon, the adaptive loop, and the dependency rule.

What each top-level directory is, in one line each.

```
client/ React renderer, feature slices, one typed API client
server/ Fastify core, thin routes over services over ports over adapters
shared/ Zod domain schemas and contract types both sides import
electron/ desktop shell, window chrome, IPC, embedded server boot
e2e/ Playwright journeys driving the real app on fake adapters
(unit and contract tests are colocated with their source)
scripts/ generators and repo checks, all wired into CI
docs/ ADRs, the generated API reference, screenshots, phase plans
```

## Build Standalone DMG

```bash
Expand All @@ -112,6 +144,7 @@ pnpm install
pnpm dev:server # Keep this running one tab
pnpm electron:dev # Run this in a different tab
pnpm test # Run tests
pnpm e2e # Run end-to-end tests
```

Set your Claude, ChatGPT or Gemini API key in Settings (gear icon) on first launch.
Expand All @@ -128,7 +161,7 @@ Set your Claude, ChatGPT or Gemini API key in Settings (gear icon) on first laun
| AI | Vercel AI SDK |
| Storage | Filesystem (Markdown + YAML) |
| Desktop | Electron (via vite-plugin-electron) |
| Testing | Vitest |
| Testing | Vitest + Playwright (e2e) |

## License

Expand Down
3 changes: 0 additions & 3 deletions client/api/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ import { streamText } from './sse'
* browser bundle.
*/

/** One turn of chat history, sent to the server as context for the next reply. */
export type ChatHistoryMessage = z.infer<typeof ChatBodySchema>['history'][number]

/** Everything streamChat needs to ask the tutor about a chapter or a selection. */
export type StreamChatParams = z.infer<typeof ChatBodySchema> & {
/** Lets the caller abort the request in flight, since the chat panel cancels one whenever the user restarts or clears the conversation. */
Expand Down
3 changes: 1 addition & 2 deletions client/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@
* endpoints.
*/

export { ApiError, apiUrl, getApiPort, initApiBase } from './http'
export type { ApiRequestInit, JsonRequestInit } from './http'
export { ApiError, getApiPort } from './http'

export { audiobookFileUrl, coverUrl, voicePreviewUrl } from './urls'

Expand Down
19 changes: 3 additions & 16 deletions client/api/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import type {
SuggestSkillsBodySchema,
UpdateProfileBodySchema,
} from '@shared/contracts'
import type { LearningProfile, Preferences } from '@shared/domain'
import type { LearningProfile } from '@shared/domain'
import type { ProfileResponse } from '@shared/responses'
import { request } from './http'
import { streamNdjson } from './sse'

Expand All @@ -25,21 +26,7 @@ export type Skill = LearningProfile['skills'][number]
/** The model and provider choice every AI-backed profile call sends. */
type AiRequest = z.infer<typeof AiRequestSchema>

/**
* The learning profile as the server answers or accepts it over the wire.
*
* This is not LearningProfile from shared/domain.ts. That type's fields are
* style and identity, the shape the profile is persisted as on disk. The
* profile route folds those two fields into a single aboutMe string before
* it answers. That aboutMe field is what every caller actually reads, so
* this module names the wire shape on its own rather than importing a type
* that would be misleading.
*/
export interface ProfileResponse {
aboutMe: string
preferences: Preferences
skills: Skill[]
}
export type { ProfileResponse }

/** Fetch the learning profile, meaning About Me, preferences, and prior knowledge skills. */
export async function getProfile(): Promise<ProfileResponse> {
Expand Down
196 changes: 0 additions & 196 deletions client/components/ui/command.tsx

This file was deleted.

Loading
Loading