Files
learning-platform/docs/data-model.md
RaymondVerhoef 5214c9db3b
Some checks failed
On Pull Request to Main / test (pull_request) Failing after 24s
On Pull Request to Main / publish (pull_request) Has been skipped
On Pull Request to Main / deploy-dev (pull_request) Has been skipped
feat: 5-day theme-level onboarding track from the "New here?" card (#30)
A self-paced onboarding track that introduces a new employee to every KB
theme in breadth (not depth), so they grasp how Respellion works day to
day and week to week. Offered as a CTA inside the Dashboard "New here?"
explainer card; always available regardless of enrollment.

Design:
- Theme is the trackable unit; the 5 "days" are a read-time presentation
  grouping, so re-chunking never loses progress. Completion is stored per
  theme in onboarding_completions.
- Per-theme overview generated lazily on first open (fast-tier
  emit_onboarding_overview tool), cached in onboarding_overviews keyed by
  theme + a topics_fingerprint that triggers regeneration when the theme's
  topic set changes.
- Reachable via /onboarding-track using the existing skipEnrollmentGate
  prop, decoupled from the 26-week curriculum (distinct from /onboarding,
  the enrollment page).

Backend:
- pb_migrations/1781200000_created_onboarding.js: two collections with
  authenticated-only rules and unique indexes; TEXT team_member_id (no
  relation) per the post-#18/#27 convention. Mirrored in
  scripts/setup-pb-collections.mjs.
- src/lib/onboardingService.js: pure helpers (orderThemes,
  distributeThemesIntoDays, computeTopicsFingerprint,
  computeOnboardingProgress, buildOnboardingPlan) + generation + I/O.
- db.js onboarding helpers use pb.filter() bindings (theme is free text).
- LLM tool + Zod schema + registry + simulation stub.

Frontend:
- src/pages/OnboardingTrack.jsx (day list, per-theme overview, completion
  banner, progress ring/day bar).
- Dashboard "New here?" card CTA + X/5-days progress chip (hidden when the
  KB has no themes).

Docs: data-model, generation-spec (§D), frontend-spec updated.

Verified: 22 new unit tests (npm test 134/134), eslint clean on changed
files, npm run build OK, PocketBase v0.30.4 boot applies the migration
(collections + unique indexes + authed rules confirmed), and a backend
contract check (upsert idempotency, unique-index guard, special-char
theme filtering).

Closes #30

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 09:08:38 +02:00

281 lines
9.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Data model: Respellion Learning Platform
## Overview
All structured data lives in **PocketBase** (SQLite). There is **no vector store**
— retrieval is computed at runtime with a local TF-IDF index over `topics`
(`src/lib/retrieval.js`).
Schema is defined by JS migrations in `pb_migrations/` (applied automatically by
the PocketBase binary) and mirrored for local bootstrap in
`scripts/setup-pb-collections.mjs`. The data-access layer is `src/lib/db.js`.
All collections use PocketBase's auto `id`, plus `created` / `updated` autodate
fields unless noted otherwise.
---
## PocketBase collections
### `topics`
Knowledge graph nodes. Created during ingestion, enriched for curriculum.
| Field | Type | Notes |
|---|---|---|
| id | text | kebab-case slug (e.g. `holacratic-roles`) |
| label | text | display name |
| type | text | `concept` · `role` · `process` (ingestion); `fact` is excluded from learning |
| description | text | 12 sentence summary |
| learning_relevance | text | `core` · `standard` · `peripheral` · `exclude` |
| relevance_locked | bool | if true, re-ingestion will not overwrite `learning_relevance` |
| theme | text | subject grouping (used by curriculum generation) |
| complexity_weight | number | 15 (curriculum ordering) |
| difficulty | text | `introductory` · `intermediate` · `advanced` |
Topics with `type='fact'` or `learning_relevance='exclude'` are filtered out of
learning, micro-learning, curriculum, and test selection.
---
### `relations`
Knowledge graph edges between topics.
| Field | Type | Notes |
|---|---|---|
| source | text | topic id |
| target | text | topic id |
| type | text | `related_to` · `depends_on` · `part_of` · `executed_by` |
Edges are de-duplicated on the `(source, target, type)` tuple.
---
### `content`
On-demand long-form learning content, one record per topic.
| Field | Type | Notes |
|---|---|---|
| topic_id | text | topic this content belongs to |
| data | json | merged object — only generated types are present |
`data` shape (each key generated independently and shallow-merged):
```json
{
"article": { "title", "intro", "sections": [{ "heading", "body" }], "keyTakeaways": [] },
"slides": [ { "title", "bullets": [], "speakerNote" } ],
"infographic": { "headline", "tagline", "stats": [{ "value", "label", "icon" }],
"steps": [{ "number", "title", "description", "icon" }], "quote", "colorTheme" }
}
```
> There is no `podcast` key. The podcast type was removed.
---
### `micro_learnings`
Generated micro-learning artifacts. One record per topic per type.
| Field | Type | Notes |
|---|---|---|
| topic_id | relation → `topics` | cascade delete |
| type | select | `concept_explainer` · `scenario_quiz` · `flashcard_set` |
| content | json | structured output, schema varies per type |
| status | select | `draft` · `published` (only `published` is visible to employees) |
**Content JSON per type:**
```json
// concept_explainer
{ "sections": [ { "title": "string", "content": "string (HTML: <p>, <ul>, <li>, <strong>)" } ] } // ≥3 sections
// scenario_quiz
{ "scenario": "string",
"options": [ { "text": "string", "isCorrect": true, "explanation": "string" } ] } // 34 options, exactly 1 correct
// flashcard_set
{ "cards": [ { "front": "string", "back": "string" } ] } // 510 cards
```
> A former `reflection_prompt` type was dropped and is no longer generated.
---
### `micro_learning_completions`
Append-only completion events. Never updated or deleted.
| Field | Type | Notes |
|---|---|---|
| team_member_id | relation → `team_members` | the employee |
| micro_learning_id | relation → `micro_learnings` | the artifact completed |
| topic_id | relation → `topics` | denormalized topic |
| type | text | type at time of completion |
| session_week | number | the user's **absolute** curriculum week (week 1 = day they enrolled) |
The 26-week slot and cycle are derived from `session_week`; there is no stored
`cycle` field.
---
### `curriculum_versions`
Versioned 26-week schedules. New version on each (re)generation.
| Field | Type | Notes |
|---|---|---|
| version_number | number | increments per generation |
| status | text | `draft` · `active` · `superseded` (exactly one `active`) |
| generation_reason | text | why this version was created |
| confirmed_by | text | admin id who activated it |
| confirmed_at | text | ISO datetime |
| schedule | json | array of 26 week objects (below) |
| coverage_stats | json | `{ themes_kb, themes_scheduled, topics_kb, topics_scheduled }` |
**`schedule[]` week object:**
```json
{ "week_number": 1, // 1..26
"theme": "string",
"topic_ids": ["topic-id"], // 1+ topic ids
"estimated_duration": 30, // 15..45 minutes
"week_rationale": "string" }
```
---
### `team_members`
Registered users with PIN auth. This is the auth + employee record.
| Field | Type | Notes |
|---|---|---|
| name | text | display name |
| pin | text | login PIN |
| role | text | `admin` or empty/`employee` |
| curriculum_started_at | date | timestamp the user enrolled (week 1 anchor); empty until enrolled |
| enrollment_status | text | `not_started` · `active` |
A user is gated through the `/onboarding` screen until `enrollment_status='active'`
(admins are exempt when heading to the admin panel).
---
### `sources`
Uploaded source documents and their extraction status.
| Field | Type | Notes |
|---|---|---|
| name | text | original filename |
| status | text | `processing` · `completed` · `failed` · `cancelled` |
| error | text | failure message, if any |
| progress | json | `{ current, total, message }` during chunked extraction |
---
### `leaderboard`
Points ledger, one row per user.
| Field | Type | Notes |
|---|---|---|
| user_id | text | team member id |
| name | text | display name |
| points | number | cumulative (+2 per correct quiz answer) |
| tests_completed | number | count of completed tests |
| learnings_completed | number | reserved counter |
Admins are filtered out of the public leaderboard at render time.
---
### `settings`
App-wide key/value store.
| Field | Type | Notes |
|---|---|---|
| key | text | setting key |
| value | text | stringified value |
---
### `llm_calls`
Best-effort telemetry for every Anthropic call (written by `callLLM`).
| Field | Type | Notes |
|---|---|---|
| task | text | logging label (e.g. `learning.article`, `chat.r42`) |
| model | text | resolved model string |
| tier | text | `fast` · `standard` · `reasoning` |
| duration_ms | number | wall-clock |
| input_tokens / output_tokens | number | usage |
| cache_read_tokens / cache_create_tokens | number | prompt-cache usage |
| stop_reason | text | `end_turn` · `tool_use` · `max_tokens` |
| ok | bool | success flag |
| error_msg | text | error, if any |
---
### `onboarding_overviews`
Cached, breadth-first per-theme overview for the onboarding track (issue #30).
One row per theme, shared across all users. Generated lazily on first open.
| Field | Type | Notes |
|---|---|---|
| theme | text | canonical theme name (from `topics.theme`); **unique** |
| content | json | validated `onboardingOverviewSchema` payload (title, what_it_is, why_it_matters, key_points, topics_covered) |
| topics_fingerprint | text | stable hash of the theme's sorted topic ids; a mismatch triggers regeneration |
Unique index on `(theme)`. Rules: authenticated-only (`@request.auth.id != ""`).
---
### `onboarding_completions`
Per-user, per-theme completion marker for the onboarding track (issue #30).
| Field | Type | Notes |
|---|---|---|
| team_member_id | text | the employee — **plain text**, not a relation (admins can delete members; orphan rows are harmless) |
| theme | text | the theme marked complete |
Unique index on `(team_member_id, theme)` → idempotent. Rules: authenticated-only.
Completion is tracked per **theme**, not per day; the 5 "days" are a read-time
presentation grouping, so re-chunking never loses progress.
---
## Dropped / legacy collections
These existed in earlier iterations and have been removed. Their `db.js` helpers
remain as deprecated no-op stubs — do not build on them:
`quiz_banks`, `quiz_results`, `quiz_cache`, `learn_progress`, and the v1
`curriculum` collection.
---
## Client-side storage (not PocketBase)
`localStorage` is used only for admin/browser-local state:
| Key | Purpose |
|---|---|
| `admin:model:{fast,standard,reasoning}` | per-tier model overrides (legacy `admin:model`) |
| `admin:use_simulation` | stub LLM responses instead of calling Anthropic |
| `kb:suggestions` | R42 graph-delta suggestion queue (managed via `kbStore`) |
| `quiz:active:{userId}` | mid-quiz flag (hides R42) |
| `chat:thread:{userId}` | R42 conversation, capped at 50 messages |
`sessionStorage.respellion_session` holds the logged-in team member id.
---
## Retrieval (no vector DB)
R42 context is built by `src/lib/retrieval.js`:
```
buildIndex(topics) → TF-IDF index over (label + description), cached by array ref
retrieveTopK(index, q, k) → top-K topics, score = Σ (1 + log(tf)) · log((N+1)/(df+1))
```
`src/components/chat/rag.js` combines top-K results with verbatim topic mentions,
filters relations to the retrieved set, and injects limited deep content for
explicitly named topics.