Add comprehensive documentation for key organizational aspects
All checks were successful
On Push to Main / test (push) Successful in 1m33s
On Push to Main / publish (push) Successful in 1m31s
On Push to Main / deploy-dev (push) Successful in 2m3s

- Introduced "Pension Scheme & Benefits" detailing secondary employment benefits and pension specifics.
- Created "Roles & Accountabilities" outlining the Holacracy role structure and responsibilities within Respellion.
- Added "Security" section covering GDPR compliance and workplace safety protocols.
- Established "Spending and Contracting" policy detailing expense categories and submission processes.
- Documented "Who We Are" to define Respellion's identity, services, and operational model under Holacracy and ISO 9001.
This commit is contained in:
RaymondVerhoef
2026-05-27 08:24:56 +02:00
parent 7066f881f9
commit 07af2783dc
26 changed files with 2631 additions and 4598 deletions

View File

@@ -2,144 +2,150 @@
Welcome, fellow AI agent! If you are reading this, you are tasked with maintaining or extending the Respellion Learning Platform. This document provides the critical context, architectural patterns, and design standards you need to successfully work on this codebase. Welcome, fellow AI agent! If you are reading this, you are tasked with maintaining or extending the Respellion Learning Platform. This document provides the critical context, architectural patterns, and design standards you need to successfully work on this codebase.
> **Last updated:** 2026-05-18Adds 52-week annual curriculum system (§12). Reflects selective content generation (3 types), ISO week alignment, GitHub sync folder change, and AI extraction token limits. > **Last updated:** 2026-05-26Per-user curriculum start (employees enroll on first login; week/cycle derived from each user's `curriculum_started_at`, no longer from the ISO calendar week). Documents the real React/Vite + PocketBase stack, TF-IDF retrieval, 3 learning-content types, 3 micro-learning types, and the tiered Claude model setup.
## 1. Architectural Overview ## 1. Architectural Overview
This is a single-page React application built with **Vite**, backed by **PocketBase** as the database and auth layer. This is a single-page React application built with **Vite**, backed by **PocketBase** as the database and auth layer. There is no separate backend server — the browser talks directly to PocketBase, and to the Anthropic API through a reverse proxy.
* **Frontend:** React, React Router, Vanilla CSS (via CSS variables) + Tailwind utility classes mapped to those variables. * **Frontend:** React 19, React Router 7, Vanilla CSS (via CSS variables) + Tailwind v4 utilities mapped to those variables.
* **Backend:** PocketBase (self-hosted). All data is stored in PocketBase collections, not localStorage. * **Backend:** PocketBase (self-hosted, SQLite). All data is stored in PocketBase collections, not localStorage.
* **Animations:** Framer Motion (used extensively for page transitions, podium effects, and gamification feedback). * **Animations:** Framer Motion (page transitions, podium effects, gamification feedback).
* **Icons:** Lucide React. * **Icons:** Lucide React.
* **Visualizations:** D3.js (used strictly for the Admin Knowledge Graph). * **Visualizations:** D3.js (used strictly for the Admin Knowledge Graph).
* **Retrieval:** A dependency-free TF-IDF index over the knowledge graph (`src/lib/retrieval.js`). There is **no Qdrant and no embeddings API** — older specs that mention them describe a design that was never built.
> The top-level `app/` directory is abandoned Next.js scaffolding from that original design. It is not built or deployed. Ignore it; the real app is `src/`.
## 2. State Management & Storage (Critical) ## 2. State Management & Storage (Critical)
All persistent data lives in **PocketBase**. The data access layer is in `src/lib/db.js`, which wraps the PocketBase SDK client from `src/lib/pb.js`. All persistent data lives in **PocketBase**. The data access layer is in `src/lib/db.js`, which wraps the PocketBase SDK client from `src/lib/pb.js`.
**PocketBase Collections:** **PocketBase Collections (current):**
* `topics` — Knowledge graph nodes (`id`, `label`, `type`, `description`). * `topics` — Knowledge graph nodes (`id`, `label`, `type`, `description`, `learning_relevance`, `relevance_locked`, `theme`, `complexity_weight`, `difficulty`).
* `relations` — Knowledge graph edges (`source`, `target`, `type`). * `relations` — Knowledge graph edges (`source`, `target`, `type``related_to` / `depends_on` / `part_of` / `executed_by`).
* `content` — AI-generated learning modules per topic (`topic_id`, `data`). The `data` field is a **merged JSON object** containing only the content types that have been generated for that topic (e.g. `{ article: {...}, slides: [...] }`). New types are shallow-merged into the existing object by `learningService.js`; nothing is ever overwritten. * `content` — AI-generated learning modules per topic (`topic_id`, `data`). The `data` field is a **merged JSON object** containing only the content types generated so far (e.g. `{ article: {...}, slides: [...] }`). New types are shallow-merged in by `learningService.js`; nothing is overwritten.
* `quiz_banks` — Banks of AI-generated questions per topic (`topic_id`, `questions[]`). * `micro_learnings` — Generated micro-learning artifacts (`topic_id`, `type`, `content`, `status`). One record per topic per type. `status='published'` items are visible to employees.
* `quiz_results` — User test scores (`user_id`, `week_number`, `score`, `total`, `percentage`, etc.). * `micro_learning_completions` — Append-only completion events (`team_member_id`, `micro_learning_id`, `topic_id`, `type`, `session_week`).
* `quiz_cache` — Cached weekly quiz for a user (`user_id`, `week_number`, `questions[]`, `meta`). * `curriculum_versions` — Versioned 26-week schedules (`version_number`, `status`, `generation_reason`, `confirmed_by`, `confirmed_at`, `schedule` JSON, `coverage_stats` JSON). Exactly one `active` at a time.
* `learn_progress` — Whether a user completed the weekly learning session (`user_id`, `week_number`, `done`). * `leaderboard` — Points ledger (`user_id`, `name`, `points`, `tests_completed`, `learnings_completed`).
* `leaderboard` — Points ledger (`user_id`, `name`, `points`, `tests_completed`). * `team_members` — Registered users with PIN auth (`name`, `role`, `pin`, `curriculum_started_at`, `enrollment_status`).
* `team_members` — Registered users with PIN auth (`name`, `role`, `pin`). * `sources` — Uploaded source documents and extraction status (`name`, `status`, `error`, `progress`).
* `sources` — Uploaded source documents and their extraction status (`name`, `status`, `error`). * `settings` — Key/value store (`key`, `value`).
* `curriculum` — Annual learning schedule (`year`, `week_number`, `topic_id`, `theme`, `quarter`, `is_review_week`, `sort_order`). One entry per week per year. Managed via the admin Curriculum tab. * `llm_calls` — Per-call telemetry (`task`, `model`, `tier`, `duration_ms`, token counts, `stop_reason`, `ok`, `error_msg`).
* `settings` — Key/value store for app-wide settings (`key`, `value`).
**Dropped collections:** `quiz_banks`, `quiz_results`, `quiz_cache`, `learn_progress`, and the legacy `curriculum` (v1) collection no longer exist. The matching `db.js` helpers are deprecated stubs — do not build on them.
**localStorage** is only used for **admin browser settings** (not user data): **localStorage** is only used for **admin browser settings** (not user data):
* `respellion:admin:anthropic_key` — Anthropic API key. * `admin:model:fast` / `admin:model:standard` / `admin:model:reasoning` — per-tier model overrides (legacy `admin:model` still honored for `standard`).
* `respellion:admin:model` — Model override. * `admin:use_simulation` — when true, `callLLM` returns stub data instead of calling Anthropic. Useful for UI work without spending tokens.
* `respellion:admin:use_simulation`Simulation mode toggle. * `kb:suggestions`Pending/approved/rejected graph deltas proposed by R42. Always mutated via `kbStore` (see §9).
* `kb:suggestions` — Pending/approved/rejected graph deltas proposed by R42. Always mutated via `kbStore` (see §8). * `quiz:active:{userId}` — Boolean flag set while a user is mid-quiz. R42's launcher is hidden when this is true (quiz-integrity rule).
* `quiz:active:{userId}` — Boolean flag set while the user is mid-quiz. R42's FAB is hidden when this is true (quiz-integrity rule).
* `chat:thread:{userId}` — Persisted R42 conversation, capped at 50 messages. * `chat:thread:{userId}` — Persisted R42 conversation, capped at 50 messages.
**Session:** User login is PIN-based. The logged-in user's ID is stored in `sessionStorage` under `respellion_session` and resolved against `team_members` on app load (see `src/store/AppContext.jsx`). **Session:** Login is PIN-based. The logged-in user's ID is stored in `sessionStorage` under `respellion_session` and resolved against `team_members` on app load (`src/store/AppContext.jsx`).
**Week Number:** The current ISO-8601 week number is calculated dynamically on app load via `getWeekNumber(new Date())` in `src/store/AppContext.jsx`. It is **not** stored in the database. The `ADVANCE_WEEK` action still exists for admin use, but initial state always reflects the real calendar week. **Per-user curriculum position (important — changed):** Each employee starts the curriculum when *they* choose. On first login a blocking onboarding screen (`/onboarding`) records `curriculum_started_at` and flips `enrollment_status` to `active`. `AppContext` derives `state.weekNumber` — an absolute counter starting at 1 — from `getPersonalWeekNumber(curriculum_started_at)` (= `floor(days_since_start / 7) + 1`). The 26-week slot and cycle come from `getCurriculumWeek(n)` (`((n-1) % 26) + 1`) and `getCurriculumCycle(n)` (`floor((n-1)/26)+1`) in `curriculumService.js`. The cycle is **detached from the ISO calendar** — week 1 is simply the first 7 days after the user's start. After week 26 the cycle restarts at week 1 with the same content. `state.weekNumber` is `0` until the user enrolls.
**Curriculum Year:** The curriculum year is derived from `new Date().getFullYear()` via `getCurriculumYear()` in `src/lib/curriculumService.js`. It is not storedalways computed. > Do **not** reintroduce ISO-week-based scheduling or a shared `admin:current_week`. There is no global "current week" anymore — every employee has their own.
**Important:** All `db.js` functions are `async`. Always `await` them — omitting `await` will silently pass a Promise where data is expected. **Auto-Cancellation:** The PocketBase JS SDK has auto-cancellation enabled by default, which aborts concurrent identical requests (common under React StrictMode and `Promise.all`) with `ClientResponseError 0`. It is **globally disabled** via `pb.autoCancellation(false)` in `src/lib/pb.js`. Never re-enable it.
**Auto-Cancellation:** The PocketBase JS SDK has auto-cancellation enabled by default. This causes concurrent identical requests (like `db.getTopics()` during React StrictMode renders or concurrent Promise.all) to abort with `ClientResponseError 0`. This feature is **globally disabled** in `src/lib/pb.js` via `pb.autoCancellation(false)` to prevent UI crashes during concurrent fetching. **PocketBase URL:** Resolved from `VITE_PB_URL`, else `window.location.origin` (`src/lib/pb.js`). In production Caddy proxies `/api/*` and `/_/*` to the PocketBase container.
**Important:** All `db.js` functions are `async`. Always `await` them — omitting `await` silently passes a Promise where data is expected.
## 3. The AI Integration (Anthropic) ## 3. The AI Integration (Anthropic)
The application calls the Anthropic API via a proxy to avoid CORS issues. All Anthropic calls go through one wrapper.
* **Location:** `src/lib/llm.js`. * **Location:** `src/lib/llm.js`, function `callLLM(...)`. Callers must never reach `/api/anthropic` directly.
* **Proxy:** In Docker, `/api/anthropic` is proxied via Caddy to the Anthropic API endpoint. In local dev, configure `vite.config.js` to proxy the same path. The API key is injected server-side by Caddy; there is **no client-side API key**. * **Proxy:** Requests hit `/api/anthropic/v1/messages`. In Docker, Caddy proxies this to `https://api.anthropic.com` and injects the `x-api-key` header server-side. In local dev, `vite.config.js` does the same using `process.env.ANTHROPIC_API_KEY`. There is **no client-side API key**.
* **Token limit:** `generateContent` uses `max_tokens: 8192`. Do not lower this — large knowledge-base files need the headroom. * **Model tiers:** `fast` = `claude-haiku-4-5-20251001`, `standard` = `claude-sonnet-4-6`, `reasoning` = `claude-opus-4-7`. Choose a tier per task; admins can override per tier from Settings.
* **JSON Enforcement:** Prompts *must* strictly enforce that Claude returns *only* raw JSON. Do not let the AI wrap the response in markdown blocks — a regex strip via `.match(/\{[\s\S]*\}/)` is already applied in all service files. * **Structured output:** Prefer Anthropic **tool use** with a forced `toolChoice`. Tool inputs are validated against Zod schemas in `src/lib/llmSchemas.js` (auto-looked-up via `toolSchemaRegistry`). For text responses, `parseStructuredText` strips code fences and extracts the outermost balanced JSON.
* **Prompt caching:** Wrap stable system text with `cachedSystem(text)` to attach `cache_control: ephemeral`.
* **Retry/limits:** `src/lib/llmRetry.js` handles exponential backoff with jitter on retryable statuses (408/425/429/5xx/529), honors `Retry-After`, and provides rate limiters (e.g. the extraction limiter caps ~20 req/min, burst 2). Default `maxTokens` is 4096; extraction and long-form content use 8192.
* **Telemetry:** Every call is logged (best-effort, non-blocking) to the `llm_calls` collection.
## 4. Design System & Aesthetics ## 4. Design System & Aesthetics
Respellion relies on a premium, modern aesthetic. Respellion relies on a premium, modern aesthetic.
* **CSS Variables:** Rely heavily on the variables defined in `src/index.css`. * **CSS Variables:** Rely on the variables in `src/index.css``var(--color-bg)`, `var(--color-paper)`, `var(--color-teal)`, `var(--color-accent)`, radii `var(--r-sm)`, `var(--r-lg)`, `var(--r-org)`.
* Colors: `var(--color-bg)`, `var(--color-paper)`, `var(--color-teal)`, `var(--color-accent)`. * **Tailwind:** Tailwind v4 utilities are mapped to these variables. Use classes like `bg-teal`, `text-fg-muted`, `border-bg-warm`. Avoid raw hex codes.
* Radii: `var(--r-sm)`, `var(--r-lg)`, `var(--r-org)` (an organic, pill-like shape used for badges and podiums). * **`stylesheet.css`** (repo root) is the authoritative visual reference and is frozen — do not edit it.
* **Tailwind:** Tailwind is mapped to these CSS variables. Avoid hardcoding random hex codes. Use the existing Tailwind classes like `bg-teal`, `text-fg-muted`, and `border-bg-warm`. * **Components:** Reuse the UI primitives in `src/components/ui/` (`Card.jsx`, `Button.jsx`, `Tag.jsx`, `Input.jsx`).
* **Components:** Always reuse the UI primitives in `src/components/ui/` (`Card.jsx`, `Button.jsx`, `Tag.jsx`, `Input.jsx`).
## 5. Learning Content Types ## 5. Learning Content Types (on-demand, per topic)
`src/lib/learningService.js` supports **three** content types for selective generation: `src/lib/learningService.js` generates **three** long-form content types into the `content` collection, on demand:
| Type | Schema key | Description | | Type | Schema key | Description |
|---|---|---| |---|---|---|
| `article` | `content.article` | Title, intro, sections, key takeaways | | `article` | `content.article` | Title, intro, sections, key takeaways |
| `slides` | `content.slides` | Array of slides with bullets and speaker notes | | `slides` | `content.slides` | Slides with bullets and speaker notes |
| `infographic` | `content.infographic` | Headline, tagline, stats, steps, quote | | `infographic` | `content.infographic` | Headline, tagline, stats, steps, quote |
**There is no podcast type.** It was removed. Do not re-add it. `generateLearningContent(topic, force, selectedType)` accepts one of the three types, or `'all'` (admin regeneration). Cache-hit logic checks `content[selectedType]` directly; new data is shallow-merged so other types are preserved. **There is no podcast type.**
`generateLearningContent(topic, force, selectedType)` accepts one of the three types above, or `'all'` (admin regeneration). Cache-hit logic checks `content[selectedType]` directly. On generation, the new data is shallow-merged into the existing cached object so other types are preserved. Article refinement uses targeted patch tools (`set_intro`, `set_section`, `add_section`, `remove_section`, `replace_takeaways`) so the model edits only what changed.
The `LearningContentViewer` tab bar reflects exactly these three modes. The empty-state for an un-generated tab shows a "Generate [Type]" button that calls `onGenerate(activeMode)` passed from the parent. ## 6. Micro-Learnings (the weekly session)
`src/lib/microLearningService.js` generates short, single-topic interactions into the `micro_learnings` collection. **Three** types are currently active (a former `reflection_prompt` type was dropped):
| Type | Tier | Shape |
|---|---|---|
| `concept_explainer` | standard | `{ sections: [{ title, content (HTML) }] }` (≥3 sections) |
| `scenario_quiz` | standard | `{ scenario, options: [{ text, isCorrect, explanation }] }` (34 options, 1 correct) |
| `flashcard_set` | fast (Haiku) | `{ cards: [{ front, back }] }` (510 cards) |
## 6. Gamification Rules `getOrGenerateMicroLearning(topicId, type)` returns a cached published record if present, else generates and stores one. Completions are recorded via `useMicroLearningCompletions` into `micro_learning_completions` (append-only). A weekly session is "done" when every required topic has at least one completed micro-learning.
If you are extending the Gamification system (`src/pages/Leaderboard.jsx`):
* Tests grant **+2 points** per correct answer (handled in `testService.js → saveTestResult`).
* A 100% score grants the **Perfectionist** badge (computed at render time in `Leaderboard.jsx`).
* Completing 1 test grants **First Steps**, completing 5 grants **Veteran**.
* Points are accumulated in the `leaderboard` collection via `db.upsertLeaderboardEntry`.
## 7. Docker & Deployment ## 7. Weekly Test
The app is fully containerized. PocketBase runs as a sidecar service. `src/lib/testService.js` builds a 5-question multiple-choice quiz for the user's current week.
* **Build:** `docker build -t respellion-app:latest .` * Primary topic comes from the active curriculum week (else a deterministic hash fallback), plus a few review topics for breadth.
* **Run:** `docker compose up -d` (see `docker-compose.yml`). * Generated in **one** `fast`-tier batch call (`emit_quiz_questions`), with quality gates: no duplicate options, no banned fillers ("all of the above"), explanations ≥20 chars, and a check that `correctIndex` isn't dominated by one position.
* **Caddy (reverse proxy):** Handles SPA fallback, injects the Anthropic API key via a `Authorization` header on `/api/anthropic/*` requests, and proxies `/pb/*` to the PocketBase service. Config lives in `Caddyfile`. * Scoring: **+2 points per correct answer** (`saveTestResult``score * 2`), written to the `leaderboard` collection.
* **PocketBase URL:** Resolved from `VITE_PB_URL` env var, or falls back to `window.location.origin + '/pb'` (see `src/lib/pb.js`).
## 8. Local File Upload ## 8. Gamification
The Admin upload panel (`src/components/admin/UploadZone.jsx`) allows admins to manually upload markdown/text files via a drag-and-drop interface. If you are extending gamification (`src/pages/Leaderboard.jsx`):
* Tests grant **+2 points** per correct answer (`testService.js → saveTestResult`).
* **Extraction pipeline (`src/lib/extractionPipeline.js`):** Calls `callLLM` with a strict JSON-only system prompt. To prevent truncated responses on large files, the prompt limits extraction to **max 15 topics** and their most important relations. If the AI returns non-JSON, the file is marked `failed` in the `sources` collection. * Badges are computed at render time: **First Steps** (1 test), **Veteran** (5 tests), **Perfectionist** (a 100% score).
* **Deduplication:** A file already in `sources` with `status: completed` will throw and not be re-processed. Delete the source record first to force a re-analysis. * Points accumulate in `leaderboard` via `db.upsertLeaderboardEntry`. Admins are excluded from the public board.
* **Do not increase topic cap beyond 15** without also verifying the `max_tokens: 8192` budget is sufficient for the expected file size.
## 9. R42 Chatbot ## 9. R42 Chatbot
The platform ships a global chatbot avatar called **R42**, rendered as the Respellion `{ r }` brand mark in three states (idle / typing / error). The platform ships a global chatbot avatar called **R42**, rendered as the Respellion `{ r }` brand mark in three states (idle / typing / error).
* **Mark component:** `src/components/ui/Mark.jsx` — pure SVG with `state`, `size`, `theme`, `brace`, `letter` props. Uses the `BallPill` font (`@font-face` in `src/index.css`), falling back to JetBrains Mono.
* **Mark component:** `src/components/ui/Mark.jsx`. Pure SVG; renders the brand mark with `state`, `size`, `theme`, `showFrame`, `brace`, `letter` props. Requires the `BallPill` font (loaded via `@font-face` in `src/index.css`, served from `public/fonts/BallPill-light.otf`). Falls back to JetBrains Mono (already loaded).
* **Chat module:** `src/components/chat/`. * **Chat module:** `src/components/chat/`.
* `ChatLauncher.jsx` — global FAB; auto-hides when `quiz:active:{userId}` is set. Listens to the `respellion:quiz-state` window event for fast updates. * `ChatLauncher.jsx` — global FAB; auto-hides when `quiz:active:{userId}` is set; listens to the `respellion:quiz-state` window event.
* `ChatWindow.jsx` 380×480 chat panel; Esc closes; renders messages from `useChat`. * `ChatWindow.jsx` — chat panel; renders messages from `useChat`; surfaces graph-delta confirmation chips.
* `useChat.js` — owns the message list, persists to `chat:thread:{userId}`, calls `anthropicApi.chat()`. `buildKbContext` is async (PocketBase), so `send()` is fully async. * `useChat.js` — owns the message list, persists to `chat:thread:{userId}` (cap 50; only the last ~12 turns are sent to the API), calls `callLLM`.
* `prompts.js` — system prompt, greeting, and the `propose_graph_delta` tool spec. * `prompts.js` the cacheable system prompt blocks, greeting, and the `propose_graph_delta` tool spec (max 3 topics / 5 relations).
* `rag.js`fetches `kb:topics` + `kb:relations` from PocketBase; lazy-loads `kb:content:{id}` only when a topic is mentioned. Returns `{ context, topics }` so `validateDelta` can reuse the fetched topics without a second round-trip. * `rag.js`builds KB context using the TF-IDF index from `src/lib/retrieval.js` (top-K topics + verbatim mentions), filters relations to retrieved topics, and validates proposed deltas (dedupe by id/label, no orphan/self relations, hard caps).
* **Multi-turn API:** `callLLM({ task, system, messages, tools })` in `src/lib/llm.js`. Returns a structured response containing extracted `toolUses` and text. No API key header — Caddy proxy injects it server-side. * **Model:** R42 runs on the `fast`/standard Claude tier (Haiku/Sonnet); low latency matters for chat.
* **Quiz-integrity rule:** `src/pages/Testen.jsx` sets `quiz:active:{userId}=true` on start and clears it on every non-quiz phase + unmount, plus dispatches a `respellion:quiz-state` event. Never bypass this — letting users ask R42 mid-quiz would break scoring. * **Quiz-integrity rule:** `src/pages/Testen.jsx` sets `quiz:active:{userId}=true` on start and clears it on every non-quiz phase + unmount, dispatching `respellion:quiz-state`. Never bypass this — letting users ask R42 mid-quiz would break scoring.
* **Graph refinement:** when R42's `tool_use` block proposes a `propose_graph_delta`, `rag.js` validates (no duplicate ids, no orphan relations, caps 3 topics / 5 relations) and surfaces a confirmation chip inline. * **Graph refinement:** when R42 proposes a `propose_graph_delta`, `rag.js` validates it and a confirmation chip appears inline. **Admin clicks Yes**`kbStore.applyDelta` writes to PocketBase immediately. **Non-admin clicks Yes**`kbStore.appendSuggestion` queues a `pending` entry in `kb:suggestions`.
* **Admin user clicks Ja**`kbStore.applyDelta` writes to PocketBase via `db.upsertTopic` / `db.addRelation` immediately. * **Admin approval UI:** `src/components/admin/SuggestionsQueue.jsx` lets admins approve (re-runs `applyDelta`) or reject queued suggestions.
* **Non-admin clicks Ja**`kbStore.appendSuggestion` queues an entry in `kb:suggestions` localStorage (status `pending`). * **kbStore:** `src/lib/kbStore.js` is the single source of truth for chatbot-path KB mutations. It dispatches `respellion:kb-updated` after writes so the D3 graph and queue refresh.
* **Admin approval UI:** `src/components/admin/SuggestionsQueue.jsx`, mounted at the top of the Knowledge Graph admin panel. Approve calls `kbStore.approveSuggestion(id)` which runs the same `applyDelta` merge (async, PocketBase) and flips status to `approved`. Reject flips to `rejected` for audit.
* **kbStore:** `src/lib/kbStore.js` is the single source of truth for KB mutations from the chatbot path. Topics/relations go to PocketBase; suggestions queue goes to localStorage. Dispatches `respellion:kb-updated` after any write so the D3 graph and the queue panel refresh without a reload.
## 10. How to Add New Features ## 10. Admin Panel
1. **Define Schema:** Add a new PocketBase collection via the Admin UI or the init script (`scripts/setup-pb-collections.mjs`). `src/pages/Admin/index.jsx` is tabbed: **Sources** (upload + extraction), **Content** (review/refine generated content), **Quizzes**, **Curriculum** (generate/preview/activate a 26-week schedule), **Graph** (D3 knowledge graph + suggestions queue), **Team** (manage members), **Settings** (model overrides, simulation toggle, smoke-test reset). Source upload lives in `src/components/admin/UploadZone.jsx` (`.txt` / `.md`, ≤5 MB).
2. **Add DB Helpers:** Add async CRUD functions in `src/lib/db.js`.
3. **Build UI:** Create components using `Card`, `Button`, and `framer-motion` for smooth entry (`animate-in fade-in slide-in-from-bottom-4`).
4. **Wire Logic:** Connect to `src/store/AppContext.jsx` if it requires global user context, otherwise manage state locally.
5. **Test:** Run the Docker stack (`docker compose up`) to ensure no build errors exist.
## 11. Known Gotchas & Decisions ## 11. How to Add New Features
* **No podcast type.** The podcast learning type was deliberately removed. The three supported types are `article`, `slides`, and `infographic`. 1. **Schema:** add a PocketBase collection via the PB Admin UI or a migration in `pb_migrations/` (and mirror it in `scripts/setup-pb-collections.mjs`).
* **Week number is computed, not stored.** Do not add a `admin:current_week` setting — the ISO week is always derived from `new Date()`. 2. **DB helpers:** add async CRUD in `src/lib/db.js`.
* **Caddy, not Nginx.** Older docs/comments may reference Nginx. The reverse proxy is Caddy. Update any references you encounter. 3. **UI:** build with `Card`, `Button`, `Tag`, and Framer Motion entry animations.
* **AI token budget.** If you see `[Pipeline] AI returned non-JSON response` in the logs, the response was truncated. Increase the topic cap prompt constraint before raising `max_tokens`. 4. **Logic:** connect to `src/store/AppContext.jsx` for global user/week context; otherwise keep state local.
* **PocketBase auto-cancellation is OFF.** `pb.autoCancellation(false)` is set globally in `src/lib/pb.js`. Never re-enable it — it causes abort errors during concurrent fetches in React StrictMode. 5. **Verify:** `npm test`, `npm run lint`, `npm run build`.
## 12. 26-Week Versioned Curriculum System ## 12. Known Gotchas & Decisions
The platform uses a **26-week perpetual curriculum cycle** so every employee covers the knowledge base in focused, thematic weekly blocks. Cycle 1 covers weeks 1-26, Cycle 2 replays the same schedule, and so on. * **Per-user curriculum start.** Week/cycle derive from each user's `curriculum_started_at`, not the calendar. There is no shared current week. New users are gated through `/onboarding` until enrolled.
* **No podcast type.** Three content types only: `article`, `slides`, `infographic`.
* **Three micro-learning types.** `concept_explainer`, `scenario_quiz`, `flashcard_set`. `reflection_prompt` was dropped.
* **No Qdrant / no embeddings.** Retrieval is local TF-IDF (`src/lib/retrieval.js`).
* **Caddy, not Nginx.** The reverse proxy is Caddy (`Caddyfile`).
* **PocketBase auto-cancellation is OFF.** Set in `src/lib/pb.js`; never re-enable.
* **Go through `callLLM`.** Never call the Anthropic proxy directly; you lose retry, schema validation, and telemetry.
* **AI token budget.** Truncation surfaces as `LLMTruncatedError` (`stop_reason: max_tokens`). For extraction, tighten the prompt's topic cap before raising `max_tokens`.
* **Service:** `src/lib/curriculumService.js` — curriculum engine, week resolution, AI generation, version lifecycle. ## 13. 26-Week Per-User Curriculum System
* **DB Collections:** `curriculum_versions` holds the generated JSON schedules. `topics` includes `theme`, `complexity_weight`, and `difficulty` for generation input. The platform uses a **26-week perpetual curriculum cycle**. Every employee covers the knowledge base in focused, thematic weekly blocks, **starting whenever they enroll**.
* **Version Lifecycle:** `draft` -> `active` -> `superseded`. Only one active version exists at a time. * **Service:** `src/lib/curriculumService.js` — week/cycle math, AI schedule generation, version lifecycle, progress.
* **Admin UI:** `src/components/admin/CurriculumManager.jsx` — accessed via the "Curriculum" tab. Admins trigger an AI-generated draft, preview the 26-week schedule, and confirm it to activate it globally. * **DB:** `curriculum_versions` holds generated JSON schedules; `topics` carry `theme`, `complexity_weight`, `difficulty` as generation input.
* **Calendar-Driven Weeks:** The current week (1-26) and cycle (1, 2, 3...) are derived via modulo arithmetic from the absolute ISO calendar week in `curriculumService.js`. All employees share the same schedule. * **Version lifecycle:** `draft``active``superseded`. Only one active version at a time (`CurriculumManager.jsx`).
* **Topic Enrichment:** The Curriculum Manager includes a one-off AI enrichment step to assign `theme` and `complexity_weight` to topics missing them before curriculum generation. * **Per-user weeks:** `getPersonalWeekNumber(startedAt)` yields an absolute week counter; `getCurriculumWeek`/`getCurriculumCycle` map it to the 126 slot and cycle. Same content each cycle.
* **Progress tracking:** `getYearProgress(userId, isoWeekNumber)` computes completion for the *current 26-week cycle*. * **Topic enrichment:** a one-off AI step assigns `theme`/`complexity_weight`/`difficulty` to topics missing them before generation (`enrichTopicsForCurriculum`, batches of 20).
* **Fallback:** If no active curriculum version exists, `getAssignedTopic()` falls back to the legacy hash-based assignment. Do not remove the hash fallback. * **Progress:** `getYearProgress(userId, personalWeekNumber)` computes completion for the current cycle.
* **Deferred Features (V2):** Per-employee staggered start dates, cycle 2+ personalization, and prerequisite DAG sorting are deferred to future iterations to keep the MVP lean. * **Fallback:** if no active version exists, `getAssignedTopic()` falls back to deterministic hash-based assignment. Keep the fallback.
Good luck! You are building a platform that empowers continuous learning. Keep it fast, keep it beautiful, and keep the user engaged. Good luck! You are building a platform that empowers continuous learning. Keep it fast, keep it beautiful, and keep the user engaged.

224
CLAUDE.md
View File

@@ -1,54 +1,16 @@
# CLAUDE.md # CLAUDE.md
## What you are building ## What this is
A mobile-first progressive web application for employee learning. Employees follow The **Respellion Learning Platform** — an internal AI-powered learning app that
a perpetual 26-week curriculum built from an internal knowledge base. An AI keeps employees current with the company's evolving knowledge base. Employees
assistant called R42 is available on every screen. Admins upload source documents follow a perpetual **26-week curriculum**, each working through weekly learning
that are processed into the knowledge base by AI. sessions and tests. An AI assistant called **R42** is available on every screen.
Admins upload source documents that Claude extracts into a knowledge graph.
Read the full design before writing any code: This is a **single-page React application** (Vite) backed by **PocketBase**.
- /docs/handover.md — all decisions made, rationale, constraints There is **no separate backend** — all logic runs in the browser and talks
- /docs/architecture.md — system design, data flows, tech stack directly to PocketBase collections and (via a proxy) the Anthropic API.
- /docs/data-model.md — all PocketBase collections, Qdrant schema, types
- /docs/ingestion-spec.md — ingestion service (build this first)
- /docs/implementation-plan.md — ordered build sequence with acceptance criteria
---
## Absolute constraints
These rules are non-negotiable and apply to every session:
1. Never modify any file listed in PROTECTED.md
2. Never modify any file outside /app — the pipeline, Dockerfile, ansible,
and docker-compose files are frozen
3. Never delete files without explicit confirmation
4. Never change package.json scripts that contain 'deploy' or 'build:prod'
5. Ask before acting when scope is unclear — do not infer intent from legacy/ code
---
## Repository structure
```
repo/
├── CLAUDE.md ← you are here
├── PROTECTED.md ← files you must never touch
├── docs/ ← spec files — read, never modify
├── legacy/ ← old prototype — read-only reference only
└── app/ ← your working directory
├── frontend/ ← Next.js 14 PWA
└── services/
├── ingestion/ ← build first
├── generation/ ← build second
├── curriculum/ ← build third
├── embedding/ ← integrated into ingestion, separate later
├── chat/ ← R42
└── progress/ ← gamification
```
All work happens inside /app. No exceptions.
--- ---
@@ -56,98 +18,124 @@ All work happens inside /app. No exceptions.
| Layer | Technology | | Layer | Technology |
|---|---| |---|---|
| Frontend | Next.js 14, TypeScript strict, Tailwind CSS, PWA | | Frontend | React 19 + Vite 8, React Router 7 |
| Backend state | PocketBase (binary, not source — do not scaffold from scratch) | | Styling | Vanilla CSS (CSS custom properties) + Tailwind v4 utilities mapped to those variables |
| Vector store | Qdrant via REST client | | Animation | Framer Motion |
| AI generation | Claude Sonnet 4 — model string: claude-sonnet-4-20250514 | | Icons | Lucide React |
| AI chat (R42) | Claude Haiku 4.5 — model string: claude-haiku-4-5-20251001 | | Graph viz | D3.js (admin knowledge graph only) |
| Embeddings | OpenAI text-embedding-3-small | | Backend / DB / auth | PocketBase (self-hosted, SQLite) |
| Services | Node.js, Fastify, TypeScript strict | | AI | Anthropic Claude via a reverse-proxy (Caddy in prod, Vite proxy in dev) |
| Validation | Zod on all external data (API responses, AI output, PocketBase) | | Retrieval (RAG) | Local TF-IDF over the knowledge graph — **no Qdrant, no embeddings API** |
| Validation | Zod on all AI tool output |
| Infra | Docker + Caddy; Ansible playbooks under `infra/` |
**Claude models** (`src/lib/llm.js`, by tier):
- `fast``claude-haiku-4-5-20251001` (R42 chat, quiz batches, flashcards)
- `standard``claude-sonnet-4-6` (extraction, article/slides/infographic, curriculum)
- `reasoning``claude-opus-4-7`
Admins can override any tier's model string from the Settings tab (persisted in
localStorage as `admin:model:{tier}`).
--- ---
## Stylesheet ## Repository structure
An existing stylesheet lives at /stylesheet.css in the repo root. This is the ```
authoritative visual style for the application. repo/
├── CLAUDE.md ← you are here
├── AI_AGENT.md ← detailed architecture/patterns guide — read this
├── README.md ← quickstart
├── PROTECTED.md ← files you must not modify
├── docs/ ← spec files (describe the system as built)
├── src/ ← THE APPLICATION (React/Vite)
│ ├── pages/ ← route screens (Dashboard, Leren, Testen, Leaderboard, Login, Onboarding, Admin/)
│ ├── components/ ← ui primitives, admin panels, chat (R42), micro_learning
│ ├── lib/ ← services: llm, db, extractionPipeline, learningService,
│ │ microLearningService, testService, curriculumService, retrieval, pb
│ ├── hooks/ ← React hooks
│ └── store/ ← AppContext (global state)
├── pb_migrations/ ← PocketBase schema migrations (JS, applied by the PB binary)
├── scripts/ ← setup-pb-collections.mjs (local collection bootstrap)
├── public/ ← static assets, fonts
├── infra/ ← Ansible deploy playbooks (development / production)
├── Caddyfile, Dockerfile, docker-compose.yml ← deployment (frozen)
└── stylesheet.css ← authoritative visual reference (frozen)
```
Rules: > **Note on `/app`:** the top-level `app/` directory is **abandoned scaffolding**
- Never modify stylesheet.css > from the original Next.js + Qdrant design that was never shipped. It is not
- Import it as a global stylesheet in the Next.js frontend > wired into `package.json`, Vite, or Docker. **Ignore it.** The real app is `/src`.
- Do not override its rules with Tailwind utility classes or inline styles
- When Tailwind and the stylesheet conflict, the stylesheet wins ---
- If a UI element is not covered by the stylesheet, use Tailwind — but match
the visual language (spacing, colour, type scale) of the existing stylesheet ## Absolute constraints
1. Never modify any file listed in `PROTECTED.md`.
2. Never modify `stylesheet.css` — it is the authoritative visual reference. Use the
CSS variables in `src/index.css` and the Tailwind classes mapped to them.
3. Treat the deployment files as frozen unless explicitly asked: `Dockerfile`,
`Caddyfile`, `docker-compose.yml`, `infra/`, `.github/workflows/`.
4. Never delete files without explicit confirmation.
5. Never re-enable PocketBase auto-cancellation — `pb.autoCancellation(false)` in
`src/lib/pb.js` is deliberate (see AI_AGENT.md §2).
6. There is **no podcast** learning type. It was removed. Do not re-add it.
7. Ask before acting when scope is unclear.
--- ---
## Code conventions ## Code conventions
- TypeScript strict mode everywhere — no `any` types - All `src/lib/db.js` functions are `async` — always `await` them.
- All Claude API responses validated through Zod before use - All Claude tool output is validated through Zod (`src/lib/llmSchemas.js`) before use.
- All PocketBase writes typed against collection schemas in data-model.md Never reach `/api/anthropic` directly — go through `callLLM` in `src/lib/llm.js`.
- Qdrant payloads explicitly typed — no untyped objects - No client-side API key. The Anthropic key is injected server-side by the proxy.
- No inline hardcoded API keys — environment variables only - Reuse the UI primitives in `src/components/ui/` (`Card`, `Button`, `Tag`, `Input`).
- REST conventions for all service APIs - Mobile-first — the employee app targets 375px width and scales up.
- Mobile-first CSS — design for 375px width, scale up - No hardcoded hex colors; use the design-system CSS variables / Tailwind tokens.
--- ---
## Build order ## Running locally
Follow /docs/implementation-plan.md exactly. ```bash
Do not skip phases. Do not build phase N+1 before phase N passes its npm install
acceptance criteria. ./pocketbase.exe serve # or the muchobien/pocketbase Docker image
node scripts/setup-pb-collections.mjs # first run only — bootstraps collections
npm run dev # Vite dev server (proxies /api/anthropic)
```
Current phase: **Phase 1 — Infrastructure + ingestion service** Set `ANTHROPIC_API_KEY` in the environment so the Vite proxy can inject it
(`vite.config.js`). PocketBase migrations in `pb_migrations/` apply automatically
when the PB binary starts.
| Command | Purpose |
|---|---|
| `npm run dev` | Vite dev server |
| `npm run build` | Production build to `dist/` |
| `npm test` | Vitest unit tests |
| `npm run lint` | ESLint |
--- ---
## Session discipline ## Documentation map
Each session has a defined scope in implementation-plan.md. - `AI_AGENT.md` — detailed patterns, gotchas, and subsystem guide. **Start here for any real work.**
At the start of each session: - `docs/architecture.md` — system design and data flows
1. State which phase and step you are working on - `docs/data-model.md` — every PocketBase collection and field
2. Read the relevant spec file(s) - `docs/ingestion-spec.md` — upload → knowledge-graph extraction
3. Propose a file structure or change plan before writing code - `docs/generation-spec.md` — learning content + micro-learning generation
4. Implement after the plan is clear - `docs/curriculum-spec.md` — 26-week per-user curriculum engine
- `docs/r42-spec.md` — the R42 chatbot
At the end of each session: - `docs/frontend-spec.md` — screens, routing, onboarding
1. State what was completed - `docs/gamification-spec.md` — points, badges, leaderboard
2. State what acceptance criteria have been met - `micro-learning-spec.md` — micro-learning learner experience
3. State what the next session should start with
--- ---
## When you are uncertain ## When you are uncertain
Check the spec files in /docs first. Check the spec files in `docs/` and `AI_AGENT.md` first. If the spec does not
If the spec does not cover it, flag the gap explicitly rather than making cover it, read the actual code in `src/` — it is the source of truth. Flag gaps
an assumption. Do not look at legacy/ code for implementation guidance — explicitly rather than assuming. Do not treat the abandoned `/app` scaffolding or
it is a different stack and will lead you in the wrong direction. `legacy`-style references in old comments as guidance.
---
## Environment variables
Each service has its own .env file. Templates are defined in each service spec.
Never commit actual values. Always use .env.example with placeholder values.
The full set across all services:
```
ANTHROPIC_API_KEY=
OPENAI_API_KEY=
POCKETBASE_URL=
POCKETBASE_ADMIN_EMAIL=
POCKETBASE_ADMIN_PASSWORD=
QDRANT_URL=
QDRANT_API_KEY=
INGESTION_PORT=3001
GENERATION_PORT=3002
CURRICULUM_PORT=3003
CHAT_PORT=3004
PROGRESS_PORT=3005
NEXT_PUBLIC_API_URL=
NEXT_PUBLIC_POCKETBASE_URL=
```

View File

@@ -1,34 +1,30 @@
# Protected files — do not modify # Protected files — do not modify
These files and directories must never be modified by Claude Code. These files and directories must never be modified by Claude Code without an
They belong to the existing deployment pipeline and are frozen. explicit request. They belong to the deployment pipeline and the authoritative
visual reference, and are otherwise frozen.
## Pipeline and CI/CD ## Deployment & CI/CD
.github/
.github/workflows/ .github/workflows/
.gitlab-ci.yml (if present)
## Container and orchestration
Dockerfile Dockerfile
docker-compose.yml docker-compose.yml
docker-compose.prod.yml Caddyfile
docker-compose.override.yml (if present) Caddyfile.test
## Infrastructure and provisioning ## Infrastructure & provisioning
ansible/ infra/ (Ansible playbooks: development / production)
nginx/
## Stylesheet ## Stylesheet
stylesheet.css (repo root — use as-is, do not modify) stylesheet.css (repo root — authoritative visual reference, use as-is)
## Environment templates at repo root ## Abandoned scaffolding (do not edit or rely on)
.env.example (repo root only — services may have their own) app/ (original Next.js + Qdrant design, never shipped — the real app is /src)
## Legacy prototype
legacy/
--- ---
The only infrastructure file that will change is Dockerfile, and only in Notes:
Phase 8 step 8.5. That change is made manually by the human, not by - The active application lives entirely in `/src`. Editing the app means editing `/src`.
Claude Code. - PocketBase schema changes go through `pb_migrations/` (and should be mirrored in
`scripts/setup-pb-collections.mjs`) — those are NOT frozen.
- If a task genuinely requires touching a frozen file (e.g. a deliberate deployment
change), confirm with the human first.

View File

@@ -4,24 +4,28 @@ An internal AI-powered learning platform that keeps Respellion employees up to d
## Features ## Features
- **Weekly Learning Station** — Each employee is assigned a topic each week (via deterministic hash of user ID + week number). They choose their preferred format: Article, Slides, or Infographic. Content is generated on-demand by Claude and cached per topic. - **Onboarding** — On first login each employee enrolls into the curriculum with one tap. Their 26-week cycle starts then and there.
- **Weekly Test** — AI-generated quiz based on the knowledge graph. Results are stored and feed the leaderboard. - **Weekly Learning Station** — Each employee gets a topic for their current curriculum week, drawn from the active 26-week schedule (with a deterministic hash fallback when no curriculum is active). They engage via micro-learnings (concept explainer, scenario quiz, flashcard set) and can also generate long-form formats (Article, Slides, Infographic) on demand.
- **Leaderboard & Gamification** — Points for correct answers, badges for streaks and perfect scores. - **Weekly Test** — A 5-question AI-generated quiz for the week's topic. Results feed the leaderboard.
- **R42 Chatbot** — An always-available AI assistant (backed by Claude) with access to the full knowledge graph. Can propose graph updates that admins approve or reject. - **Leaderboard & Gamification** — Points for correct answers, badges for milestones (First Steps, Veteran, Perfectionist).
- **Admin Panel** — Manage the knowledge graph, upload source files, review generated content, refine it with AI, and monitor team progress. - **R42 Chatbot** — An always-available AI assistant (Claude) grounded in the knowledge graph via local TF-IDF retrieval. It can propose graph updates that admins approve or reject.
- **Admin Panel** — Manage the knowledge graph, upload source files, review/refine generated content, generate the curriculum, and manage the team.
## Tech Stack ## Tech Stack
| Layer | Technology | | Layer | Technology |
|---|---| |---|---|
| Frontend | React 18 + Vite | | Frontend | React 19 + Vite 8, React Router 7 |
| Styling | Vanilla CSS (custom properties) + Tailwind utility classes | | Styling | Vanilla CSS (custom properties) + Tailwind v4 utilities |
| Animations | Framer Motion | | Animations | Framer Motion |
| Icons | Lucide React | | Icons | Lucide React |
| Graph viz | D3.js (admin knowledge graph only) | | Graph viz | D3.js (admin knowledge graph only) |
| Backend / DB | PocketBase (self-hosted) | | Backend / DB / auth | PocketBase (self-hosted, SQLite) |
| Retrieval | Local TF-IDF (no Qdrant, no embeddings API) |
| AI | Anthropic Claude (via Caddy reverse proxy) | | AI | Anthropic Claude (via Caddy reverse proxy) |
| Infra | Docker + Caddy | | Infra | Docker + Caddy; Ansible playbooks under `infra/` |
**Claude models (by tier):** `fast` = `claude-haiku-4-5-20251001`, `standard` = `claude-sonnet-4-6`, `reasoning` = `claude-opus-4-7`.
## Getting Started (local dev) ## Getting Started (local dev)
@@ -29,14 +33,17 @@ An internal AI-powered learning platform that keeps Respellion employees up to d
# 1. Install dependencies # 1. Install dependencies
npm install npm install
# 2. Start PocketBase (Windows) # 2. Start PocketBase
./pocketbase.exe serve ./pocketbase.exe serve
# 3. Start the dev server # 3. Bootstrap collections (first run only)
node scripts/setup-pb-collections.mjs
# 4. Start the dev server
npm run dev npm run dev
``` ```
The Vite dev server proxies `/api/anthropic` and `/pb` — see `vite.config.js`. Set `ANTHROPIC_API_KEY` in your environment — the Vite dev server proxies `/api/anthropic` and injects the key server-side (see `vite.config.js`). PocketBase migrations in `pb_migrations/` apply automatically when the binary starts.
## Deployment (Docker) ## Deployment (Docker)
@@ -46,24 +53,26 @@ docker compose up -d
The `Caddyfile` handles: The `Caddyfile` handles:
- SPA fallback routing - SPA fallback routing
- `/pb/*` → PocketBase sidecar - `/api/anthropic/*` → Anthropic API (server-side API key injection)
- `/api/anthropic/*` → Anthropic API (with server-side API key injection) - `/api/*` and `/_/*` → the PocketBase sidecar
## Key Files ## Key Files
| File | Purpose | | File | Purpose |
|---|---| |---|---|
| `src/lib/learningService.js` | Selective content generation (article / slides / infographic) | | `src/lib/llm.js` | Core Anthropic wrapper (`callLLM`): tiers, retry, schema validation, telemetry |
| `src/lib/extractionPipeline.js` | Uploaded file → knowledge graph extraction |
| `src/lib/llm.js` | Core Anthropic LLM wrapper |
| `src/lib/db.js` | All PocketBase data access | | `src/lib/db.js` | All PocketBase data access |
| `src/store/AppContext.jsx` | Global state; computes ISO week number on load | | `src/lib/extractionPipeline.js` | Uploaded file → knowledge-graph extraction |
| `src/components/admin/UploadZone.jsx` | Drag-and-drop file upload UI | | `src/lib/learningService.js` | On-demand content generation (article / slides / infographic) |
| `src/lib/microLearningService.js` | Micro-learning generation (3 types) |
| `src/lib/curriculumService.js` | 26-week per-user curriculum engine |
| `src/lib/retrieval.js` | TF-IDF retrieval for R42 |
| `src/store/AppContext.jsx` | Global state; derives the user's curriculum week from their start date |
| `AI_AGENT.md` | Detailed context guide for AI coding agents | | `AI_AGENT.md` | Detailed context guide for AI coding agents |
## Content Types ## Content Types
Learning content is generated **on demand per type** and merged into the cached object: On-demand long-form content (the `content` collection), generated per type and merged into the cached object:
| Type | Key in DB | Description | | Type | Key in DB | Description |
|---|---|---| |---|---|---|
@@ -73,7 +82,10 @@ Learning content is generated **on demand per type** and merged into the cached
> The podcast type was removed. Do not re-add it. > The podcast type was removed. Do not re-add it.
Micro-learnings (the `micro_learnings` collection): `concept_explainer`, `scenario_quiz`, `flashcard_set`.
## Documentation ## Documentation
- **`AI_AGENT.md`** — Full architectural guide for AI coding agents (patterns, gotchas, decisions). - **`CLAUDE.md`** — project overview and constraints for AI coding agents.
- **`CHANGELOG.md`** — PocketBase upstream changelog (not application changelog). - **`AI_AGENT.md`** — full architectural guide (patterns, gotchas, decisions).
- **`docs/`** — subsystem specs (architecture, data model, ingestion, generation, curriculum, R42, frontend, gamification).

View File

@@ -1,279 +1,179 @@
# Architecture: employee learning platform # Architecture: Respellion Learning Platform
## Overview ## Overview
A mobile-first progressive web application that provides employees with a structured knowledge library, a 26-week perpetual learning curriculum, and an AI-powered assistant (R42). The knowledge base is the single source of truth for all content, micro learnings, curriculum scheduling, and chat retrieval. A mobile-first single-page web application that gives employees a structured
knowledge library, a 26-week per-user learning curriculum, and an AI assistant
(R42). The **knowledge graph** stored in PocketBase is the single source of truth
for all content, micro-learnings, curriculum scheduling, and chat retrieval.
Unlike the original design (a Next.js multi-service system with Qdrant), the
shipped platform is a **React/Vite SPA talking directly to PocketBase**, with the
Anthropic API reached through a thin reverse proxy. All application logic — AI
orchestration, retrieval, generation, scoring — runs in the browser.
```
Browser (React SPA)
├── PocketBase SDK ───────────────► PocketBase (SQLite): all structured data + auth + files
└── callLLM ──► /api/anthropic ──► Anthropic API (key injected by the proxy)
(Caddy in prod / Vite proxy in dev)
```
--- ---
## System domains ## Runtime topology
### Admin app
Browser-based interface for content administrators.
Responsibilities:
- Upload source documents (PDF, MD, TXT)
- Review and approve AI-generated Theme batches
- Edit and finetune AI-generated curriculum
- Confirm curriculum regeneration after KB updates
- Monitor ingestion and generation job status
### Employee app
Mobile-first PWA accessible on all devices.
Responsibilities:
- Weekly session delivery (Theme + Topics + micro learning type selection)
- Knowledge library (browse all published Topics)
- Gamification profile (heatmap, badges, streak, leaderboard)
- R42 chatbot (available on every screen)
### Backend services
Six discrete services, each with a single responsibility.
| Service | Responsibility |
|---|---|
| Ingestion service | Document upload → chunk → extract KB structure |
| Generation service | Topics → 10 micro learning types (structured JSON) |
| Curriculum service | KB graph → 26-week schedule, versioning, regeneration |
| Embedding service | Chunks + topic summaries → Qdrant |
| Chat service (R42) | Query → vector retrieval → grounded response |
| Progress service | Completions → XP → badges → streak |
---
## Deployment topology
``` ```
repo/ ┌─────────────────────────────┐ ┌──────────────────────────┐
├── .github/workflows/ ← pipeline (frozen) │ Caddy container │ │ PocketBase container │
├── docker-compose.yml ← infrastructure (frozen) - serves built SPA (/srv) │ /api/*│ - SQLite data │
├── Dockerfile ← updated once to point at /app - /api/anthropic/* → Claude │───────►│ - auth (team_members) │
├── ansible/ ← provisioning (frozen) - /api/*, /_/* → PocketBase │ /_/* - file storage │
├── legacy/ ← original prototype (read-only reference) - injects ANTHROPIC_API_KEY │ - migrations (pb_migrations)
└── app/ └─────────────────────────────┘ └──────────────────────────┘
├── frontend/ ← Next.js PWA (admin + employee)
└── services/
├── ingestion/
├── generation/
├── curriculum/
├── embedding/
├── chat/
└── progress/
``` ```
In local dev, `vite.config.js` replaces Caddy: it proxies `/api/anthropic` to
`https://api.anthropic.com` and injects `ANTHROPIC_API_KEY`. PocketBase runs
directly (`./pocketbase.exe serve`).
--- ---
## Tech stack ## Tech stack
| Layer | Technology | Rationale | | Layer | Technology | Rationale |
|---|---|---| |---|---|---|
| Frontend | Next.js 14, TypeScript, Tailwind CSS | PWA support, single codebase for admin + employee | | Frontend | React 19 + Vite 8, React Router 7 | Fast SPA, single codebase for admin + employee |
| Backend state | PocketBase | Auth, file storage, admin UI, SQLite — no infra overhead | | Styling | CSS variables + Tailwind v4 | Premium design system; Tailwind mapped to variables |
| Vector store | Qdrant (Docker) | RAG retrieval, runs as single container | | Backend state | PocketBase (SQLite) | Auth, file storage, admin UI — no infra overhead |
| AI generation | Claude Sonnet 4 via Anthropic API | Structured JSON output, long-form drafting, graph reasoning | | Retrieval | Local TF-IDF (`src/lib/retrieval.js`) | Grounds R42 with zero external vector infra |
| AI chat (R42) | Claude Haiku 4.5 via Anthropic API | Low latency, cost-effective, grounded by retrieval layer | | AI | Claude via Anthropic API (proxied) | Structured tool output, long-form drafting, chat |
| Embeddings | OpenAI text-embedding-3-small | Cost-effective, high quality at this scale | | Auth | PocketBase `team_members` + PIN | Lightweight internal auth, role = admin / (employee) |
| Auth | PocketBase built-in | Role-based: admin / employee | | Infra | Docker + Caddy, Ansible (`infra/`) | Containerized deploy to dev/prod |
There is **no Qdrant, no OpenAI/embeddings service, and no separate Node backend.**
--- ---
## AI model responsibilities ## AI model responsibilities
| Task | Model | `callLLM` (`src/lib/llm.js`) selects a Claude model by **tier**:
|---|---|
| Document → KB structure extraction | Claude Sonnet 4 | | Tier | Model | Used for |
| Topic body drafting | Claude Sonnet 4 | |---|---|---|
| Micro learning generation (all 10 types) | Claude Sonnet 4 | | `fast` | `claude-haiku-4-5-20251001` | R42 chat, weekly quiz batch, flashcard sets |
| Curriculum generation + versioning | Claude Sonnet 4 | | `standard` | `claude-sonnet-4-6` | KB extraction, article/slides/infographic, micro-learnings, curriculum generation |
| R42 chat responses | Claude Haiku 4.5 | | `reasoning` | `claude-opus-4-7` | reserved for heavier reasoning tasks |
| Embeddings | text-embedding-3-small |
Admins can override the model string per tier from the Settings tab.
--- ---
## Document ingestion pipeline ## Knowledge ingestion pipeline
``` ```
Admin uploads file (PDF / MD / TXT) Admin uploads .txt / .md (≤5 MB) in the Sources tab
Format detection → text extraction extractionPipeline.js chunks the text (~8000 chars, 800 overlap)
MD: split on headings → preserve hierarchy
PDF: pdfplumber → page + paragraph detection
TXT: sliding window chunking with overlap
Chunk cleaning (strip headers/footers/artefacts) Per chunk: callLLM (standard tier) with the emit_knowledge_graph tool
→ topics (id, label, type, description, learning_relevance)
→ relations (source, target, type)
Claude Sonnet 4 reads chunks → extracts: Results merged into the `topics` and `relations` collections
- candidate Themes (topic id de-dup; relevance_locked topics keep their relevance)
- candidate Topics per Theme
- Topic→Topic relationships (related, prerequisite, contrast)
- key terms for glossary
Draft KB written to PocketBase (status: draft) Source status tracked in `sources` (processing → completed / failed / cancelled)
Embedding service: embed source chunks → write to Qdrant
Admin reviews Theme batch → approves / edits / rejects
On approval: Topics published, micro learning generation queued
Curriculum regeneration notification queued for admin
``` ```
Note: embeddings are generated from **source chunks**, not only from AI-generated topic summaries. R42 retrieves from grounded source material. There are no embeddings. Retrieval for R42 is computed on the fly with TF-IDF
over `topics` (`label + description`).
MD source files are the preferred format for admins — heading structure maps directly to Theme → Topic hierarchy and improves extraction quality. See `docs/ingestion-spec.md`.
--- ---
## Curriculum lifecycle ## Content generation
Two generators, both via `callLLM` with forced tool use and Zod-validated output:
- **Long-form content** (`learningService.js`) → `content` collection. Three types
generated on demand and shallow-merged: `article`, `slides`, `infographic`.
- **Micro-learnings** (`microLearningService.js`) → `micro_learnings` collection.
Three types: `concept_explainer`, `scenario_quiz`, `flashcard_set`.
See `docs/generation-spec.md`.
---
## Curriculum lifecycle (per-user)
### Generation ### Generation
Input: all published Themes, Topics, relationship graph, complexity weights Input: published topics grouped by `theme`, ordered by `complexity_weight`.
Process: cluster by Theme → sequence pedagogically (prerequisites first, complexity gradient) → distribute across 26 weeks → ensure full KB coverage `curriculumService.generateCurriculumDraft()` asks Claude for a 26-week schedule
Output: versioned 26-week draft schedule via `emit_curriculum_schedule`, validates it, and stores a `curriculum_versions`
row (`status='draft'`). Admin previews and confirms → `active`. Only one active
version exists; the prior active becomes `superseded`.
### Perpetual cycling ### Per-user cycling
The curriculum runs continuously. After week 26, the employee begins cycle 2 on the latest curriculum version. The curriculum is **not** anchored to the calendar. Each employee enrolls when
they choose (first-login onboarding), which sets `team_members.curriculum_started_at`.
Their position is derived:
Second and subsequent cycles are not identical to cycle 1: ```
- Theme sequence is varied personalWeek = floor(daysSinceStart / 7) + 1 // absolute counter, ≥1
- Recommended micro learning types surface types the employee has not yet used curriculumWeek = ((personalWeek - 1) % 26) + 1 // 1..26 slot
- Topics with low engagement in prior cycles receive increased coverage cycle = floor((personalWeek - 1) / 26) + 1 // 1, 2, 3, ...
```
### Versioning rules After week 26 the cycle restarts at week 1 with the **same** content.
| Event | Action | See `docs/curriculum-spec.md`.
|---|---|
| New source doc published to KB | Regenerate curriculum from week N+1 for all active employees |
| Topic body edited | Micro learnings regenerated; curriculum unaffected |
| Theme batch approved | Regeneration queued; admin confirms before it applies |
Completed weeks are immutable. Regeneration only affects future unstarted weeks.
### Admin regeneration flow
Admin receives notification: "N new topics added. Regenerate curriculum? This will update unstarted weeks for all active employees."
Admin can preview the proposed new schedule before confirming.
--- ---
## Weekly session flow (employee) ## Weekly session flow (employee)
``` ```
Week N opens Enroll (first login) → curriculum_started_at set
Employee sees assigned Theme + Topics for the week Dashboard shows current cycle / week / assigned topic
Per Topic: employee selects micro learning type Learning Station: complete ≥1 micro-learning for the week's topic(s)
(all published types for that topic are available)
Employee completes one or more types per topic Weekly Test: 5 AI-generated questions → +2 points per correct answer
Completion recorded → XP awarded → badges evaluated Leaderboard updates; badges evaluated at render time
Progress visible on public leaderboard and activity feed
``` ```
Sessions support multiple micro learning types per topic in a single session.
---
## Micro learning types
All 10 types are generated by Claude Sonnet 4 as structured JSON, stored in PocketBase, and rendered by the frontend. One or more types may be published per topic.
| # | Type | Format |
|---|---|---|
| 1 | Concept explainer | 23 paragraphs + example |
| 2 | Scenario quiz | situation + 34 MCQ options + explained answers |
| 3 | Common misconceptions | 35 false beliefs + corrections |
| 4 | Step-by-step how-to | numbered procedure |
| 5 | Comparison card | side-by-side on 46 dimensions |
| 6 | Reflection prompt | open question + model answer |
| 7 | Spaced repetition flashcards | 510 Q&A pairs |
| 8 | Case study mini-analysis | 150200 word scenario + guiding questions |
| 9 | Glossary anchor | term + definition + correct use + misuse |
| 10 | Myth vs. evidence | false claim + evidence-based rebuttal |
--- ---
## R42 — chat service design ## R42 — chat service design
R42 is a functional KB-grounded assistant available on every screen in the employee app. R42 is a KB-grounded assistant on every screen (`src/components/chat/`).
Behaviour: - Persists the conversation per user in `localStorage` (`chat:thread:{userId}`, cap 50; ~12 turns sent to the API).
- Stateless per session (no memory between conversations) - Builds context with the TF-IDF index (top-K topics + verbatim mentions), injects related relations and limited deep content.
- Retrieves relevant chunks from Qdrant using the employee's query - Can propose a `propose_graph_delta` (≤3 topics, ≤5 relations). Admins apply directly; non-admins queue a suggestion for admin approval.
- Knows the employee's current curriculum week → retrieval is context-weighted - Hidden during quizzes (the `quiz:active:{userId}` integrity rule).
- Cites source topic in every response ("based on the **Holacratic roles** topic")
- Explicitly refuses to answer outside KB scope rather than hallucinating
- Scope: internal KB only
Implementation: See `docs/r42-spec.md`.
- Employee query → embed → Qdrant nearest-neighbour retrieval → top-K chunks
- Chunks + employee context injected into Haiku 4.5 prompt
- Response streamed to frontend
UI: floating button bottom-right, unobtrusive on mobile.
--- ---
## Gamification system ## Gamification
Inspired by the visual language of GitHub, Stack Overflow, and Duolingo. Mechanics use developer-native terminology. - Points: +2 per correct quiz answer, stored in `leaderboard`.
- Badges (render-time): First Steps (1 test), Veteran (5 tests), Perfectionist (100% score).
- Leaderboard excludes admins.
### XP unit: commits See `docs/gamification-spec.md`.
Every completed topic earns commits. Quantity varies by micro learning type complexity.
### Levels
`Intern → Junior → Medior → Senior → Staff → Principal`
Based on cumulative commits across all cycles.
### Streak
Counted in consecutive weeks, not days. Resets if a week is skipped entirely.
### Activity heatmap
GitHub-style contribution graph spanning the full 26-week cycle. Cell darkness = number of types completed that week.
### Badges
| Tier | Condition |
|---|---|
| Bronze | Complete any session |
| Silver | 5 sessions completed, 5 different types used |
| Gold | 13 sessions without skipping a week |
| Legendary | All 26 sessions, all 10 types used at least once |
Named content badges (examples):
- `governance nerd` — all holacratic structure topics completed
- `process architect` — all internal process topics completed
- `deep reader` — case study type used 5+ times
### Milestone cards (public)
At weeks 13 and 26, a public card is posted to the shared activity feed:
```
🚀 [Name] shipped the full curriculum
26 weeks · [N] commits · [badges]
Longest streak: [N] weeks
```
Language: shipping vocabulary, not school vocabulary.
### Leaderboard
Not ranked 1N by score. Displays multiple dimensions:
| Employee | Commits | Streak | Types used | Badges |
|---|---|---|---|---|
Multiple paths to visibility. No single metric determines standing.
--- ---
## Security and privacy ## Security and privacy
- Auth: PocketBase role-based (admin / employee) - Auth: PocketBase `team_members` with PIN; role `admin` unlocks the Admin panel.
- Gamification data (commits, badges, streak) is public to all employees - The Anthropic API key never reaches the client — it is injected by Caddy / the Vite proxy.
- Session completion data (which topic, which type, when) is public - R42 conversations are stored client-side per user; no server-side chat history.
- Source documents are admin-only - Source documents and the knowledge graph are managed by admins.
- No PII beyond display name stored in gamification context
- R42 is stateless — no chat history persisted

View File

@@ -1,407 +1,86 @@
# Curriculum service spec # Curriculum spec: 26-week per-user cycle
## Responsibility The curriculum sequences the knowledge base into a 26-week schedule. Every
employee runs their **own** cycle, starting when they enroll. Implemented in
Generates a versioned 26-week learning schedule from the published knowledge `src/lib/curriculumService.js`; admin UI in
base. Manages perpetual cycling, version transitions, and employee curriculum `src/components/admin/CurriculumManager.jsx`.
state. Handles regeneration when the KB changes.
--- ---
## Service location ## Data
``` - **`curriculum_versions`** — generated schedules. Lifecycle `draft → active →
app/services/curriculum/ superseded`; exactly one `active`. The `schedule` JSON is an array of 26 week
├── src/ objects: `{ week_number (126), theme, topic_ids[], estimated_duration (1545),
│ ├── index.ts entry point, Fastify server week_rationale }`. `coverage_stats` records theme/topic coverage.
│ ├── routes/ - **`topics`** — supply `theme`, `complexity_weight` (15), and `difficulty` as
│ │ ├── curriculum.ts POST /generate, GET /current, GET /preview generation input.
│ │ └── employee.ts GET /state/:userId, POST /advance/:userId
│ ├── generator/
│ │ ├── build.ts KB graph → 26-week schedule (AI call)
│ │ ├── sequence.ts prerequisite + complexity ordering
│ │ └── cycle.ts cycle 2+ variation logic
│ ├── versioning/
│ │ ├── apply.ts apply new version to active employees
│ │ └── freeze.ts protect completed weeks
│ └── lib/
│ ├── pocketbase.ts
│ └── anthropic.ts
├── package.json
├── tsconfig.json
└── .env.example
```
--- ---
## API surface ## Topic enrichment (prerequisite)
### POST /generate Generation needs themed, weighted topics. `enrichTopicsForCurriculum()` finds
topics missing a `theme` (excluding `type='fact'` and `learning_relevance='exclude'`)
Triggers curriculum generation from current published KB. and, in batches of 20, calls Claude with `emit_topic_enrichment` to assign
Called by admin app after confirming regeneration. `theme`, `complexity_weight`, and `difficulty`. Triggered from the Curriculum tab.
Request:
```json
{
"triggeredBy": "string",
"reason": "new_topics" | "manual"
}
```
Response (202 Accepted):
```json
{
"jobId": "string",
"status": "queued"
}
```
--- ---
### GET /preview ## Generation
Returns proposed new curriculum before admin confirms. `generateCurriculumDraft(reason)`:
Called by admin app to show preview before regeneration is applied. 1. Group learning topics by `theme` (`buildThemeTopicMap`), sorted by
`complexity_weight` ascending.
2. Build a prompt describing themes and their topic ids. If there are more than 26
themes, the model is instructed to **merge** closely related themes.
3. `callLLM` (standard tier, `maxTokens: 8192`, temp 0) with forced
`emit_curriculum_schedule`. Up to 2 attempts; on validation failure the errors
are fed back into the retry prompt.
4. Validate (`validateSchedule`): exactly 26 weeks, correct `week_number` sequence,
durations 1545, every `topic_id` exists, ≥1 topic per week. Unscheduled themes
are warnings, not hard errors.
5. Supersede any existing draft and store the new `draft` version with coverage stats.
Response: `confirmVersion(versionId, adminUserId)` activates a draft (and supersedes the old
```json active version); `rejectVersion` discards a draft.
{
"version": 3,
"weeks": [
{
"weekNumber": 1,
"theme": { "id": "string", "title": "string" },
"topics": [
{ "id": "string", "title": "string", "complexityWeight": 2 }
],
"estimatedDurationMinutes": 25
}
],
"coverageStats": {
"themesTotal": 8,
"themesCovered": 8,
"topicsTotal": 42,
"topicsCovered": 42
}
}
```
--- ---
### GET /current ## Per-user scheduling (the cycle)
Returns the currently active curriculum version with all week slots. The cycle is **detached from the calendar**. Enrollment sets
`team_members.curriculum_started_at` (see `docs/frontend-spec.md` for onboarding).
`AppContext` derives the user's position:
```js
getPersonalWeekNumber(startedAt) // floor(daysSinceStart / 7) + 1, ≥1 (0 if not enrolled)
getCurriculumWeek(personalWeek) // ((n - 1) % 26) + 1 → 1..26 slot
getCurriculumCycle(personalWeek) // floor((n - 1) / 26) + 1 → 1, 2, 3, ...
```
- Week 1 begins the day the employee enrolls.
- After week 26 the cycle restarts at week 1 with the **same** content.
- `state.weekNumber` (the absolute counter) is `0` until the user enrolls; pages are
gated behind onboarding so they never render with week 0.
--- ---
### GET /state/:userId ## Content & progress for a week
Returns an employee's current curriculum state. - `getCurrentWeekContent(personalWeek)` reads the active version's schedule, maps the
26-week slot to its `topic_ids`, and returns `{ cycle, weekNumber, theme, topics,
Response: estimatedDuration, rationale }`.
```json - `getAssignedTopic(userId, week)` returns the week's primary topic, falling back to
{ a deterministic hash of `userId:week` when no curriculum is active. **Keep the
"userId": "string", fallback.**
"currentCycle": 1, - `getYearProgress(userId, personalWeek)` computes completion for the current cycle.
"currentWeek": 7,
"startDate": "2026-01-15T00:00:00Z",
"activeVersionId": "string",
"nextSessionTheme": { "id": "string", "title": "string" },
"nextSessionTopics": []
}
```
--- ---
### POST /advance/:userId ## Notes
Called by progress service when an employee completes a week. - There is no shared "current week" and no `admin:current_week` setting.
Increments currentWeek, handles cycle transition at week 26. - Regeneration produces a new version; activating it changes future weeks for all
users. Completion history (`micro_learning_completions`) is append-only and never
Request: rewritten.
```json
{
"completedWeek": 7
}
```
---
## Curriculum generation
### Input
All published Themes and Topics retrieved from PocketBase:
```typescript
type KBSnapshot = {
themes: {
id: string
title: string
description: string
topics: {
id: string
title: string
complexityWeight: number // 15
difficulty: string
prerequisiteTopics: string[] // topic IDs
relatedTopics: string[]
contrastTopics: string[]
}[]
}[]
}
```
---
### Pre-processing: sequence topics within themes
Before the AI call, the service resolves topic ordering within each Theme
using a topological sort on prerequisite relationships.
```
For each Theme:
Build directed graph: prerequisite_topics edges
Topological sort → ordered topic list
If cycle detected (should not occur but handle): log warning, fall back to
complexity_weight ascending order
```
This pre-processing means the AI does not need to reason about prerequisites —
it receives already-ordered topic lists and focuses on Theme sequencing.
---
### AI call: Theme sequencing across 26 weeks
System prompt:
```
You are a curriculum designer. Your task is to distribute a set of learning
Themes across 26 weekly sessions to create an effective learning journey.
Output ONLY valid JSON matching the schema provided. No preamble, no
explanation, no markdown fences.
Rules:
- Every Theme must appear at least once across 26 weeks
- Themes with more Topics (higher topic count) may span multiple weeks or
appear in multiple cycles within the 26 weeks
- Sequence Themes so foundational concepts precede dependent ones
- Distribute complexity progressively: introductory Themes early, advanced
Themes in the second half
- If total Topics across all Themes exceeds what 26 weeks can cover in depth,
prioritise breadth in cycle 1 — every Theme covered, key Topics per Theme
- Assign an estimated duration in minutes per week (1545 minutes per session)
- Return exactly 26 week slots
```
User prompt:
```
Knowledge base snapshot:
{KBSnapshot as JSON}
Generate a 26-week curriculum schedule.
```
Output schema:
```typescript
type CurriculumDraft = {
weeks: {
weekNumber: number // 126
themeId: string
topicIds: string[] // ordered subset of theme's topics
estimatedDurationMinutes: number
rationale: string // one sentence — shown to admin in preview
}[]
}
```
AI call configuration:
```typescript
{
model: 'claude-sonnet-4-20250514',
max_tokens: 4000,
temperature: 0
}
```
Validation: Zod schema on output. Check all themeIds and topicIds exist in
the KB snapshot before writing. Reject and retry once on validation failure.
---
### Write to PocketBase
```
Create curriculum_versions record {
version: latest + 1,
status: 'draft',
generated_at: now,
generation_notes: reason
}
For each week in CurriculumDraft:
Create curriculum_weeks record {
curriculum_version: versionId,
week_number: weekNumber,
theme: themeId,
topics: topicIds,
topic_order: [0, 1, 2, ...],
estimated_duration_minutes: value,
admin_notes: ''
}
Set curriculum_versions.status → 'draft'
Notify admin: preview available at GET /preview
```
Draft version is not applied until admin confirms via POST /generate confirm.
---
## Versioning and regeneration
### Applying a new version
When admin confirms, `apply.ts` runs:
```
Get all employees from employee_curriculum_state
For each employee:
frozenWeek = employee.current_week
Update employee_curriculum_state:
active_version = new version ID
Note: completed weeks are protected by current_week value
The frontend only renders weeks >= current_week from active_version
Weeks < current_week are rendered from session_completions history
(immutable records — not from curriculum_weeks)
Set old curriculum_versions.status → 'superseded'
Set new curriculum_versions.status → 'active'
```
Completed weeks are never stored against a curriculum version — they live
in session_completions. The version only determines future week content.
---
## Perpetual cycling
### Week 26 completion → cycle transition
When progress service calls POST /advance/:userId with completedWeek: 26:
```
employee.currentCycle += 1
employee.currentWeek = 1
employee.startDate = now
employee.activeVersion = current active version
Generate cycle variant (see below)
```
### Cycle variant generation
Cycle 2+ is not identical to cycle 1. The AI call receives additional context:
Additional fields in user prompt for cycle 2+:
```json
{
"cycleNumber": 2,
"employeeHistory": {
"typesUsed": ["concept_explainer", "scenario_quiz", "how_to"],
"typesNotUsed": ["case_study", "myth_vs_evidence", "comparison_card"],
"lowEngagementTopics": ["topic-id-1", "topic-id-2"]
}
}
```
Additional rules added to system prompt for cycle 2+:
```
- Vary the Theme sequence from the previous cycle
- Topics identified as low engagement should appear earlier in this cycle
- The rationale field should note what is different from cycle 1
```
Low engagement is determined by: topics where the employee completed only
one micro learning type (minimum engagement). Retrieved from session_completions
by progress service and passed to curriculum service on cycle transition.
---
## Admin curriculum editor
The curriculum editor in the admin app (built in frontend phase) calls:
- GET /preview to display the proposed schedule
- PATCH /weeks/:weekId to update theme or topic assignment
- POST /confirm to apply the version
The PATCH route allows admin to:
- Reassign a Theme to a different week (swap two weeks)
- Add or remove Topics from a week's topic list
- Edit admin_notes per week
Changes made via PATCH update the draft curriculum_weeks records before
the version is confirmed and applied.
---
## Environment variables
```
ANTHROPIC_API_KEY=
POCKETBASE_URL=
POCKETBASE_ADMIN_EMAIL=
POCKETBASE_ADMIN_PASSWORD=
CURRICULUM_PORT=3003
```
---
## Dependencies
```json
{
"dependencies": {
"fastify": "^4",
"@anthropic-ai/sdk": "^0.24",
"pocketbase": "^0.21",
"zod": "^3",
"uuid": "^9"
}
}
```
---
## TypeScript strict mode requirements
- No `any` types
- KBSnapshot typed explicitly — validated against PocketBase response
- CurriculumDraft validated through Zod before any PocketBase writes
- Topological sort implemented with explicit typed graph structure
---
## What this service does NOT do
- Does not generate micro learnings → generation service
- Does not record completions → progress service
- Does not serve KB content → frontend reads PocketBase directly
- Does not handle auth → PocketBase + frontend
---
## Testing checkpoints
1. Generate curriculum from a KB with 5+ themes → confirm 26 weeks produced
2. Confirm all themes appear at least once
3. Confirm topic order within a week respects prerequisites
4. Add a new theme to KB → trigger regeneration → confirm employee at week 5
sees weeks 15 unchanged, weeks 626 updated
5. Advance employee through week 26 → confirm cycle 2 starts with varied sequence
6. Admin edits week 3 theme → confirm patch updates draft before confirmation

View File

@@ -1,397 +1,252 @@
# Data model: employee learning platform # Data model: Respellion Learning Platform
## Overview ## Overview
Two storage systems: All structured data lives in **PocketBase** (SQLite). There is **no vector store**
- **PocketBase** — all structured relational data (SQLite under the hood) — retrieval is computed at runtime with a local TF-IDF index over `topics`
- **Qdrant** — all vector embeddings for RAG retrieval (`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 ## PocketBase collections
### `source_documents`
Uploaded source files. Parent of all generated KB content.
| Field | Type | Notes |
|---|---|---|
| id | string | PocketBase auto |
| filename | string | original filename |
| file | file | PocketBase file storage |
| format | select | `pdf` `md` `txt` |
| status | select | `processing` `processed` `failed` |
| ingested_at | datetime | |
| chunk_count | number | total chunks extracted |
| created_by | relation → `users` | admin who uploaded |
---
### `themes`
Top-level content groupings. One Theme = one weekly session.
| Field | Type | Notes |
|---|---|---|
| id | string | PocketBase auto |
| title | string | |
| description | text | AI drafted, admin editable |
| status | select | `draft` `published` |
| source_documents | relation[] → `source_documents` | which docs contributed |
| approved_by | relation → `users` | admin who approved batch |
| approved_at | datetime | |
| created_at | datetime | |
| updated_at | datetime | |
---
### `topics` ### `topics`
Atomic knowledge units. Always belong to a Theme. Knowledge graph nodes. Created during ingestion, enriched for curriculum.
| Field | Type | Notes | | Field | Type | Notes |
|---|---|---| |---|---|---|
| id | string | PocketBase auto | | id | text | kebab-case slug (e.g. `holacratic-roles`) |
| theme | relation → `themes` | parent theme | | label | text | display name |
| title | string | | | type | text | `concept` · `role` · `process` (ingestion); `fact` is excluded from learning |
| body | text (rich) | AI drafted, admin editable | | description | text | 12 sentence summary |
| difficulty | select | `introductory` `intermediate` `advanced` | | learning_relevance | text | `core` · `standard` · `peripheral` · `exclude` |
| complexity_weight | number | 15, used by curriculum generator | | relevance_locked | bool | if true, re-ingestion will not overwrite `learning_relevance` |
| status | select | `draft` `published` | | theme | text | subject grouping (used by curriculum generation) |
| related_topics | relation[] → `topics` | lateral relationships | | complexity_weight | number | 15 (curriculum ordering) |
| prerequisite_topics | relation[] → `topics` | must-complete-first | | difficulty | text | `introductory` · `intermediate` · `advanced` |
| contrast_topics | relation[] → `topics` | deliberate opposites |
| key_terms | json | string[] — feeds glossary |
| qdrant_chunk_ids | json | string[] — references to embedded chunks |
| created_at | datetime | |
| updated_at | datetime | |
Relationship types (related / prerequisite / contrast) are stored via the three explicit relation fields rather than a generic relationship table. This keeps queries simple at this scale. 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` ### `micro_learnings`
Generated content artifacts. One record per topic per type. Generated micro-learning artifacts. One record per topic per type.
| Field | Type | Notes | | Field | Type | Notes |
|---|---|---| |---|---|---|
| id | string | PocketBase auto | | topic_id | relation → `topics` | cascade delete |
| topic | relation → `topics` | | | type | select | `concept_explainer` · `scenario_quiz` · `flashcard_set` |
| type | select | see type enum below | | content | json | structured output, schema varies per type |
| content | json | structured output — schema varies per type | | status | select | `draft` · `published` (only `published` is visible to employees) |
| status | select | `queued` `generated` `published` `rejected` |
| generation_model | string | model version used |
| generated_at | datetime | |
| published_at | datetime | |
| updated_at | datetime | |
**Type enum:** **Content JSON per type:**
`concept_explainer` `scenario_quiz` `misconceptions` `how_to` `comparison_card` `reflection_prompt` `flashcard_set` `case_study` `glossary_anchor` `myth_vs_evidence`
**Content JSON schemas per type:**
```json ```json
// concept_explainer // concept_explainer
{ { "sections": [ { "title": "string", "content": "string (HTML: <p>, <ul>, <li>, <strong>)" } ] } // ≥3 sections
"paragraphs": ["string", "string"],
"example": "string"
}
// scenario_quiz // scenario_quiz
{ { "scenario": "string",
"scenario": "string", "options": [ { "text": "string", "isCorrect": true, "explanation": "string" } ] } // 34 options, exactly 1 correct
"options": [
{ "label": "A", "text": "string", "correct": true, "explanation": "string" }
]
}
// misconceptions
{
"items": [
{ "misconception": "string", "correction": "string" }
]
}
// how_to
{
"steps": [
{ "number": 1, "instruction": "string" }
]
}
// comparison_card
{
"subject_a": "string",
"subject_b": "string",
"dimensions": [
{ "label": "string", "a": "string", "b": "string" }
]
}
// reflection_prompt
{
"prompt": "string",
"model_answer": "string"
}
// flashcard_set // flashcard_set
{ { "cards": [ { "front": "string", "back": "string" } ] } // 510 cards
"cards": [
{ "question": "string", "answer": "string" }
]
}
// case_study
{
"scenario": "string",
"questions": ["string"]
}
// glossary_anchor
{
"term": "string",
"definition": "string",
"correct_use": "string",
"misuse": "string"
}
// myth_vs_evidence
{
"myth": "string",
"evidence": "string",
"sources": ["string"]
}
``` ```
> 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` ### `curriculum_versions`
Versioned 26-week schedule. New version created on each regeneration. Versioned 26-week schedules. New version on each (re)generation.
| Field | Type | Notes | | Field | Type | Notes |
|---|---|---| |---|---|---|
| id | string | PocketBase auto | | version_number | number | increments per generation |
| version | number | increments on each regeneration | | status | text | `draft` · `active` · `superseded` (exactly one `active`) |
| status | select | `draft` `active` `superseded` | | generation_reason | text | why this version was created |
| generated_at | datetime | | | confirmed_by | text | admin id who activated it |
| approved_by | relation → `users` | admin who confirmed regeneration | | confirmed_at | text | ISO datetime |
| approved_at | datetime | | | schedule | json | array of 26 week objects (below) |
| generation_notes | text | why this version was created | | coverage_stats | json | `{ themes_kb, themes_scheduled, topics_kb, topics_scheduled }` |
Only one version has status `active` at any time. **`schedule[]` week object:**
--- ```json
{ "week_number": 1, // 1..26
### `curriculum_weeks` "theme": "string",
Individual week slots. Child of a curriculum version. "topic_ids": ["topic-id"], // 1+ topic ids
"estimated_duration": 30, // 15..45 minutes
| Field | Type | Notes | "week_rationale": "string" }
|---|---|---|
| id | string | PocketBase auto |
| curriculum_version | relation → `curriculum_versions` | |
| week_number | number | 126 |
| theme | relation → `themes` | |
| topics | relation[] → `topics` | ordered subset of theme topics |
| topic_order | json | number[] — explicit ordering |
| estimated_duration_minutes | number | AI estimate |
| admin_notes | text | freeform admin annotation |
---
### `employee_curriculum_state`
Tracks each employee's position in the curriculum. One record per employee.
| Field | Type | Notes |
|---|---|---|
| id | string | PocketBase auto |
| user | relation → `users` | |
| current_cycle | number | starts at 1, increments on loop |
| current_week | number | 126 |
| start_date | datetime | rolling start |
| active_version | relation → `curriculum_versions` | version employee is on |
| updated_at | datetime | |
When curriculum regenerates: `active_version` updates for all employees whose `current_week` is less than the first regenerated week.
---
### `session_completions`
Immutable completion records. One record per employee per topic per type.
| Field | Type | Notes |
|---|---|---|
| id | string | PocketBase auto |
| user | relation → `users` | |
| topic | relation → `topics` | |
| micro_learning | relation → `micro_learnings` | specific type completed |
| week_number | number | curriculum week at time of completion |
| cycle | number | which cycle |
| completed_at | datetime | |
Records are never updated or deleted. This is the canonical history.
---
### `gamification_profiles`
One record per employee. Updated by progress service on each completion.
| Field | Type | Notes |
|---|---|---|
| id | string | PocketBase auto |
| user | relation → `users` | |
| total_commits | number | cumulative XP |
| current_level | select | `intern` `junior` `medior` `senior` `staff` `principal` |
| current_streak_weeks | number | consecutive weeks with ≥1 completion |
| longest_streak_weeks | number | all-time high |
| types_used | json | string[] — which of 10 types used at least once |
| last_active_week | number | used to detect streak breaks |
| updated_at | datetime | |
---
### `badges`
Badge definitions. Seeded at startup, not user-generated.
| Field | Type | Notes |
|---|---|---|
| id | string | PocketBase auto |
| key | string | unique slug e.g. `governance_nerd` |
| tier | select | `bronze` `silver` `gold` `legendary` `content` |
| label | string | display name |
| description | string | award condition description |
| icon | string | emoji or icon key |
---
### `employee_badges`
Junction: which employees have earned which badges.
| Field | Type | Notes |
|---|---|---|
| id | string | PocketBase auto |
| user | relation → `users` | |
| badge | relation → `badges` | |
| earned_at | datetime | |
| cycle | number | which cycle it was earned in |
---
### `milestone_cards`
Public milestone events at weeks 13 and 26.
| Field | Type | Notes |
|---|---|---|
| id | string | PocketBase auto |
| user | relation → `users` | |
| cycle | number | |
| week | number | 13 or 26 |
| total_commits | number | snapshot at time of milestone |
| streak_weeks | number | snapshot |
| badge_keys | json | string[] — badges held at milestone |
| created_at | datetime | public feed ordered by this |
---
## PocketBase users collection (extended)
Standard PocketBase `users` collection with additional fields:
| Field | Type | Notes |
|---|---|---|
| role | select | `admin` `employee` |
| display_name | string | used in gamification feed |
| avatar | file | optional |
---
## Qdrant collections
### `source_chunks`
Embeddings of raw source document chunks. Primary retrieval target for R42.
| Field | Type | Notes |
|---|---|---|
| id | string | UUID |
| vector | float[] | 1536 dimensions (text-embedding-3-small) |
| source_document_id | string | reference to PocketBase |
| chunk_index | number | position within document |
| text | string | raw chunk text |
| theme_id | string | assigned theme (post-extraction) |
| topic_id | string | assigned topic (post-extraction, nullable) |
| format | string | pdf / md / txt |
### `topic_summaries`
Embeddings of AI-generated topic body text. Secondary retrieval target.
| Field | Type | Notes |
|---|---|---|
| id | string | UUID |
| vector | float[] | 1536 dimensions |
| topic_id | string | reference to PocketBase |
| theme_id | string | |
| title | string | for display in R42 citation |
| text | string | full topic body |
---
## Retrieval strategy for R42
R42 queries both Qdrant collections and merges results:
```
Employee query
Embed query → text-embedding-3-small
Qdrant search: source_chunks (top 5) + topic_summaries (top 3)
Filter: boost chunks from employee's current week theme
Merge + deduplicate by topic_id
Top-K context injected into Haiku 4.5 prompt
Response includes: answer + cited topic title(s)
``` ```
Source chunks are weighted higher than topic summaries to keep R42 grounded in original source material rather than AI-generated abstractions.
--- ---
## Indexes and query patterns ### `team_members`
Registered users with PIN auth. This is the auth + employee record.
Critical query patterns the data model must support efficiently: | Field | Type | Notes |
| Query | Collection | Index |
|---|---|---| |---|---|---|
| All published topics for a theme | topics | theme + status | | name | text | display name |
| All micro learnings for a topic | micro_learnings | topic + status | | pin | text | login PIN |
| Employee's current week | employee_curriculum_state | user | | role | text | `admin` or empty/`employee` |
| Weeks for a curriculum version | curriculum_weeks | curriculum_version + week_number | | curriculum_started_at | date | timestamp the user enrolled (week 1 anchor); empty until enrolled |
| Employee completion history | session_completions | user + cycle | | enrollment_status | text | `not_started` · `active` |
| Public leaderboard | gamification_profiles | total_commits + streak |
| Milestone feed | milestone_cards | created_at DESC |
| Badges earned by employee | employee_badges | user |
PocketBase creates indexes on relation fields by default. Composite indexes on `status` fields should be added manually where query frequency warrants it. A user is gated through the `/onboarding` screen until `enrollment_status='active'`
(admins are exempt when heading to the admin panel).
--- ---
## Data flow summary ### `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 |
---
## 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`:
``` ```
source_documents buildIndex(topics) → TF-IDF index over (label + description), cached by array ref
└── (ingestion service) retrieveTopK(index, q, k) → top-K topics, score = Σ (1 + log(tf)) · log((N+1)/(df+1))
└── qdrant: source_chunks
└── themes (draft)
└── topics (draft)
└── (approval)
└── topics (published)
└── qdrant: topic_summaries
└── micro_learnings (queued → published)
└── (curriculum service)
└── curriculum_versions
└── curriculum_weeks
└── (employee progress)
└── session_completions
└── gamification_profiles
└── employee_badges
└── milestone_cards
``` ```
`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.

View File

@@ -1,701 +1,93 @@
# Frontend spec # Frontend spec
## Responsibility A React 19 SPA built with Vite, routed by React Router 7. Entry: `src/main.jsx`
`src/App.jsx`. Global state lives in `src/store/AppContext.jsx`.
Single Next.js 14 codebase serving two distinct role-based experiences:
- `/admin/*` — content administration (document upload, KB review, curriculum)
- `/app/*` — employee learning experience (sessions, library, R42, gamification)
Mobile-first. Designed for 375px width, scales up. Installable as a PWA.
--- ---
## Location ## Routing & access control (`src/App.jsx`)
``` | Route | Screen | Access |
app/frontend/ |---|---|---|
├── src/ | `/login` | `Login.jsx` | public |
│ ├── app/ Next.js app router | `/onboarding` | `Onboarding.jsx` | logged-in, not yet enrolled |
│ │ ├── layout.tsx root layout — global stylesheet import | `/` | `Dashboard.jsx` | enrolled user |
│ │ ├── page.tsx redirect → role-based landing | `/learn` | `Leren.jsx` | enrolled user |
│ │ ├── admin/ | `/test` | `Testen.jsx` | enrolled user |
│ │ │ ├── layout.tsx admin shell (sidebar nav) | `/leaderboard` | `Leaderboard.jsx` | enrolled user |
│ │ │ ├── page.tsx admin dashboard | `/admin/*` | `Admin/index.jsx` | `role === 'admin'` |
│ │ │ ├── documents/
│ │ │ │ └── page.tsx document upload + ingestion status `ProtectedRoute`:
│ │ │ ├── knowledge/ - redirects to `/login` if not authenticated;
│ │ │ │ ├── page.tsx theme batch review list - redirects to `/onboarding` if `enrollment_status !== 'active'`**except** admins
│ │ │ │ └── [themeId]/page.tsx theme detail + topic edit heading to the admin panel, who are exempt;
│ │ │ └── curriculum/ - enforces `requireAdmin` for `/admin`.
│ │ │ └── page.tsx curriculum editor + regeneration
│ │ ├── app/ Navigation chrome (top bar + mobile bottom nav) is rendered by `ProtectedRoute`.
│ │ │ ├── layout.tsx employee shell (bottom nav + R42) `ChatLauncher` (R42) is mounted globally.
│ │ │ ├── page.tsx redirect → /app/session
│ │ │ ├── session/
│ │ │ │ └── page.tsx current week session
│ │ │ ├── library/
│ │ │ │ ├── page.tsx knowledge library browse
│ │ │ │ └── [topicId]/page.tsx topic detail
│ │ │ └── profile/
│ │ │ └── page.tsx gamification profile + heatmap + badges
│ │ ├── auth/
│ │ │ └── page.tsx login (PocketBase auth)
│ │ └── api/ Next.js API routes (thin proxies only)
│ ├── components/
│ │ ├── admin/ admin-specific components
│ │ ├── employee/ employee-specific components
│ │ ├── micro-learnings/ one component per micro learning type
│ │ ├── r42/ R42 chatbot components
│ │ ├── gamification/ heatmap, badges, leaderboard
│ │ └── ui/ shared primitives
│ ├── lib/
│ │ ├── pocketbase.ts PocketBase client (browser)
│ │ ├── services.ts typed API calls to backend services
│ │ ├── auth.ts auth helpers + role guards
│ │ └── hooks/ custom React hooks
│ └── types/
│ └── index.ts shared TypeScript types
├── public/
│ ├── manifest.json PWA manifest
│ ├── sw.js service worker (generated)
│ └── icons/ PWA icons (192, 512)
├── next.config.js
├── tailwind.config.ts
├── tsconfig.json
└── .env.example
```
--- ---
## Stylesheet integration ## Auth & global state (`AppContext.jsx`)
`/stylesheet.css` lives at the repo root — not inside `app/frontend/`. - Loads `team_members` on mount; auto-creates an `Admin` (PIN `0000`) if the table is empty.
- PIN login resolves a member and stores the id in `sessionStorage.respellion_session`.
Import it as the first global stylesheet in `src/app/layout.tsx`: - `state.currentUser` holds the member; `state.weekNumber` is the user's **absolute
curriculum week**, derived from `curriculum_started_at` via `getPersonalWeekNumber`
```tsx (0 until enrolled).
import '../../../stylesheet.css' // path from app/frontend/src/app/ - `enrollCurrentUser()` stamps `curriculum_started_at = now`, sets
import './globals.css' // Tailwind directives second `enrollment_status = 'active'`, and updates state.
```
Rules:
- stylesheet.css is the authoritative visual style — never override it
- Where Tailwind utility classes conflict with stylesheet.css rules,
stylesheet.css wins
- Tailwind is used for layout, spacing, and elements not covered by the
stylesheet — match the visual language (spacing scale, colour, type) of
the existing stylesheet when doing so
- Inspect stylesheet.css before implementing any component — use its CSS
custom properties (if any) rather than hardcoding values
--- ---
## PWA configuration ## Onboarding (`Onboarding.jsx`)
### next.config.js A blocking first-login screen. One CTA — "Start my journey" — calls
`enrollCurrentUser()` and routes to `/`. Week 1 begins immediately. Users already
Use `next-pwa` package to generate service worker and manifest wiring: enrolled are redirected to `/`. See `docs/curriculum-spec.md`.
```javascript
const withPWA = require('next-pwa')({
dest: 'public',
register: true,
skipWaiting: true,
disable: process.env.NODE_ENV === 'development'
})
module.exports = withPWA({
reactStrictMode: true,
})
```
### public/manifest.json
```json
{
"name": "Learning Platform",
"short_name": "Learn",
"description": "Employee knowledge and learning",
"start_url": "/app",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#ffffff",
"orientation": "portrait",
"icons": [
{ "src": "/icons/192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/512.png", "sizes": "512x512", "type": "image/png" }
]
}
```
Note: set `theme_color` and `background_color` to match stylesheet.css
primary background after inspecting the file.
### Service worker caching strategy
- Static assets: cache-first
- PocketBase API calls: network-first, fall back to cache
- Backend service calls: network-only (no caching for dynamic content)
--- ---
## Auth ## Employee screens
PocketBase handles auth. Two roles: `admin` and `employee`. - **Dashboard** — current cycle/week, assigned topic, cycle progress ring, quick
links to Learn and Test, mini leaderboard, recent activity.
- **Learning Station (`/learn`)** — the week's required topic + the rest of the
knowledge library; opening a topic shows the micro-learning selector
(`src/components/micro_learning/`). Completing ≥1 micro-learning marks the week done.
- **Test (`/test`)** — 5-question quiz with a 5-minute timer, per-question feedback,
and a results/review screen. Sets the `quiz:active` flag to hide R42.
- **Leaderboard (`/leaderboard`)** — podium + ranked list + badges (see
`docs/gamification-spec.md`).
### Login flow Labels show `Cycle X · Week Y of 26`, where Y/X come from `getCurriculumWeek` /
`getCurriculumCycle` applied to `state.weekNumber`.
```
/auth page → email + password form
PocketBase authWithPassword()
Store token in PocketBase SDK (persists in localStorage)
Read user.role from auth record
role === 'admin' → redirect to /admin
role === 'employee' → redirect to /app
```
### Route guards
Implement as Next.js middleware (`middleware.ts` at app root):
```typescript
// Admin routes: require role === 'admin'
// Employee routes: require role === 'employee'
// Unauthenticated: redirect to /auth
// Wrong role: redirect to correct landing
```
### PocketBase client (browser)
```typescript
// lib/pocketbase.ts
import PocketBase from 'pocketbase'
export const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL)
```
Use `pb.authStore` for auth state. Use `pb.collection().getFullList()` etc.
for direct PocketBase reads. The frontend reads KB content (topics, micro
learnings) directly from PocketBase — it does not proxy through backend services.
### Service calls
Backend services (ingestion, generation, curriculum, chat, progress) are called
via typed fetch wrappers in `lib/services.ts`:
```typescript
// Example
export async function postComplete(payload: CompletePayload) {
const res = await fetch(`${PROGRESS_URL}/complete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
if (!res.ok) throw new Error(`Complete failed: ${res.status}`)
return res.json() as Promise<CompleteResponse>
}
```
All service response types imported from `types/index.ts`.
--- ---
## Admin app ## Admin panel (`Admin/index.jsx`)
### Shell layout (`admin/layout.tsx`) Tabbed: **Sources** (upload + extraction), **Content** (review/refine generated
content), **Quizzes**, **Curriculum** (generate/preview/activate a schedule),
Sidebar navigation on desktop, top navigation on mobile. **Graph** (D3 knowledge graph + R42 suggestions queue), **Team** (manage members),
**Settings** (per-tier model overrides, simulation toggle, smoke-test reset).
Nav items:
- Documents
- Knowledge base
- Curriculum
- (link back to employee app)
### Documents page (`admin/documents/page.tsx`)
**Upload section**
- Drag-and-drop file input: accepts .pdf, .md, .txt
- On upload: POST file to PocketBase storage →
then POST to ingestion service `/ingest` with document metadata
- Show upload confirmation with filename
**Job status list**
- Poll GET /status/:jobId every 3 seconds while status is not done/failed
- Show per-job progress:
- Status badge: queued / extracting / chunking / structuring / embedding / done / failed
- Progress bar derived from chunksEmbedded / chunksTotal
- On done: "N themes, N topics ready for review" → link to knowledge base
- On failed: error reason in red, no retry (admin re-uploads)
- Stop polling when status === 'done' or 'failed'
**Document history**
- List of all source_documents from PocketBase
- Columns: filename, format, status, ingested_at, chunk_count
--- ---
### Knowledge base page (`admin/knowledge/page.tsx`) ## Design system
Lists all Themes with status indicator. - CSS variables in `src/index.css`: colors (`--color-bg`, `--color-paper`,
`--color-teal`, `--color-accent`), radii (`--r-sm`, `--r-lg`, `--r-org`).
**Theme card** - Tailwind v4 utilities map to those variables (`bg-teal`, `text-fg-muted`,
``` `border-bg-warm`, …). Avoid raw hex.
[Theme title] [status badge: draft / published] - `stylesheet.css` (repo root) is the authoritative visual reference — frozen.
N topics · from: filename.pdf - Reuse `src/components/ui/` primitives: `Card`, `Button`, `Tag`, `Input`.
[Approve batch] [Edit] [Reject] - Framer Motion for entry animations; mobile-first (target 375px).
```
Approve batch:
- Calls PocketBase to set theme.status → 'published', all child topics → 'published'
- Triggers generation service: POST /generate-all with themeId
- Shows toast: "Generation queued for N topics"
Reject:
- Sets theme.status → 'rejected'
- Removes from list
Edit → navigates to `/admin/knowledge/[themeId]`
**Theme detail page (`admin/knowledge/[themeId]/page.tsx`)**
Displays all Topics in the Theme as editable cards.
Topic card fields (all editable inline):
- title (text input)
- body (textarea — rich enough for paragraphs, no full rich text editor needed)
- difficulty (select: introductory / intermediate / advanced)
- key_terms (tag input — comma-separated)
- related_topics (multi-select from published topics)
- prerequisite_topics (multi-select)
Save button per card — calls PocketBase PATCH on the topic record.
Below topic list: [Approve batch] button — approves all topics in the theme.
**Micro learning generation status**
After batch approval, show generation status per topic:
```
Concept explainer ✓ published
Scenario quiz ⏳ generating
Comparison card ✓ published
...
```
Poll micro_learnings collection filtered by topic until all 10 are published.
--- ---
### Curriculum page (`admin/curriculum/page.tsx`) ## Build (`vite.config.js`)
**Current curriculum view** - Injects `__BUILD_SHA__` / `__BUILD_TIME__` (shown by `BuildStamp`).
26 weeks displayed as a list. Each week shows: - Dev server proxies `/api/anthropic` to Anthropic and injects `ANTHROPIC_API_KEY`.
``` - Source maps on; production build outputs to `dist/`.
Week 7
[Theme: Holacratic roles]
Topics: Role definitions · Circle structure · Lead link responsibilities
Estimated: 25 min
[Edit week] [Admin notes]
```
**Regeneration banner**
When a pending regeneration is queued:
```
⚠ 8 new topics added. A new curriculum version is ready to preview.
[Preview changes] [Confirm regeneration] [Dismiss]
```
Preview: shows proposed schedule with diff highlighting — weeks that changed
are highlighted, weeks that stay the same are dimmed.
Confirm: calls POST /generate confirm on curriculum service →
applies new version to all active employees.
**Drag-to-reorder**
Each week row is draggable. Reordering calls PATCH /weeks/:weekId on the
curriculum service to swap theme assignments.
**Admin notes**
Inline text input per week — saved to curriculum_weeks.admin_notes.
---
## Employee app
### Shell layout (`app/layout.tsx`)
Bottom navigation bar (mobile-first):
```
[Session] [Library] [Profile]
```
R42 floating button: fixed position, bottom-right, above the nav bar.
Z-index above all content.
### Session page (`app/session/page.tsx`)
**Week header**
```
Week 7 of 26 · Cycle 1
[Theme title: Holacratic roles]
[Progress bar: N of 26 weeks complete]
```
**Topic list**
Each topic in the week's theme rendered as a card:
```
[Topic title]
[difficulty badge] [estimated: 10 min]
Choose how to learn this topic:
[Concept explainer] [Scenario quiz] [How-to] ...
(only published types shown as buttons)
[Completed types: ✓ Concept explainer]
```
Selecting a type opens the micro learning inline (no navigation — expands in
place on mobile). Employee reads/completes it, then taps [Mark complete].
On mark complete:
- POST to progress service `/complete`
- Response displays: commits earned + any new badges as a toast notification
- Topic card updates to show type as completed (✓)
- All types in topic completable in one session
**Week complete state**
When all topics in the week have at least one completed type:
```
🚀 Week 7 complete
You earned N commits
[Continue to Week 8]
```
Continue button calls POST /advance/:userId on curriculum service.
---
### Micro learning components
One component per type in `components/micro-learnings/`.
Each receives the `content` JSON field from the micro_learnings record.
| Component | Key interactions |
|---|---|
| ConceptExplainer | Render paragraphs + example — read only |
| ScenarioQuiz | Select option → reveal explanation — stateful |
| Misconceptions | Accordion: tap misconception to reveal correction |
| HowTo | Numbered steps — tap step to check it off |
| ComparisonCard | Two-column table — swipeable on mobile |
| ReflectionPrompt | Open text area → reveal model answer on submit |
| FlashcardSet | Flip card interaction — swipe through deck |
| CaseStudy | Scenario text + open questions — read only |
| GlossaryAnchor | Term card with definition + examples |
| MythVsEvidence | Myth card → tap to reveal evidence |
All components are self-contained. They receive content JSON and emit an
`onComplete` callback. They do not call any services directly.
```typescript
type MicroLearningProps = {
content: unknown // typed per component
onComplete: () => void
}
```
---
### Knowledge library (`app/library/page.tsx`)
**Browse view**
All published topics, grouped by Theme.
Search input: filters by title and key_terms in real time (client-side).
Filter chips: by difficulty (introductory / intermediate / advanced).
Each topic shown as a card:
```
[Topic title]
[Theme] · [difficulty badge]
[key terms as chips]
```
Tap → navigate to topic detail.
**Topic detail (`app/library/[topicId]/page.tsx`)**
```
[Topic title]
[Theme] · [difficulty]
[Topic body — rendered as paragraphs]
Key terms: [chip] [chip] [chip]
Related topics: [card] [card]
Prerequisite for: [card] [card]
How to learn this topic:
[micro learning type buttons — same as session view]
```
Completing a micro learning from the library records the completion via
progress service. Week_number is set to the employee's current week.
---
### Profile page (`app/profile/page.tsx`)
**Header**
```
[Display name]
[Level badge: Junior] [N commits]
[Current streak: 5 weeks] [Longest: 8 weeks]
```
**Heatmap**
GitHub-style contribution graph.
26 columns (weeks) × rows implied by completions per week.
Cell colour: 0 completions = lightest, 5+ completions = darkest.
Tap a cell → tooltip: "Week N · N completions".
Scrollable horizontally on mobile if needed.
Implementation: render as SVG or CSS grid — no charting library required.
```typescript
// Data from GET /profile/:userId → heatmap[]
// Colour scale: 4 levels based on completions count
// 0: var(--heatmap-0)
// 1: var(--heatmap-1)
// 2-3: var(--heatmap-2)
// 4+: var(--heatmap-3)
// Use CSS custom properties — values derived from stylesheet.css palette
```
**Badges**
Grid of earned badges. Unearned badges shown as locked (greyed out).
Tap badge → tooltip with award condition.
```
🥉 First commit ✓
🥈 Five sessions ✓
🥇 On a streak 🔒 (13 week streak needed)
⭐ Shipped 🔒
---
🏷 Governance nerd ✓
🏷 Deep reader 🔒 (3/5 case studies)
```
**Leaderboard tab**
Toggle between "My profile" and "Leaderboard".
Leaderboard: table of all employees from GET /leaderboard.
Columns: Name · Commits · Streak · Types used · Badges · Level.
Not ranked 1N. No sorting by the user — display order is commits descending.
Current employee row is highlighted.
**Activity feed tab**
Third tab: "Feed".
Milestone cards from GET /feed.
Most recent first.
```
🚀 Alex shipped the full curriculum
26 weeks · 847 commits · 3 badges
Longest streak: 18 weeks
[timestamp]
```
---
## R42 chatbot components (`components/r42/`)
### R42Button
Fixed position, bottom-right, above bottom nav bar.
Circle button with R42 label or icon.
Tap → opens R42Drawer.
```tsx
// Position: fixed, bottom: calc(nav-height + 16px), right: 16px
// Z-index: above all content, below modals
```
### R42Drawer
Slides up from bottom on mobile (sheet pattern).
On desktop: expands to a side panel.
```
[R42 header bar] [close ×]
─────────────────────────────────────────────
[Response area — scrollable]
Based on: [Holacratic roles ×] [Circle structure ×]
─────────────────────────────────────────────
[Type a question...] [Send →]
```
**State machine:**
```
idle → loading (query sent) → streaming → done
↘ out_of_scope
```
**Streaming implementation:**
```typescript
// POST /chat with fetch, read SSE stream
const response = await fetch(`${CHAT_URL}/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, userId })
})
const reader = response.body!.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
const lines = decoder.decode(value).split('\n')
for (const line of lines) {
if (!line.startsWith('data: ')) continue
const event = JSON.parse(line.slice(6))
if (event.type === 'chunk') appendText(event.text)
if (event.type === 'citations') setCitations(event.topics)
if (event.type === 'out_of_scope') setOutOfScope(event.text)
if (event.type === 'done') setDone()
}
}
```
**Citations**
Rendered as tappable pills below the response.
Tap → closes R42Drawer, navigates to `/app/library/[topicId]`.
**Out of scope response**
Render as a muted message (not an error state):
"This doesn't appear to be covered in the knowledge base.
You can browse the full library in the Knowledge section."
**Stateless by design**
Conversation cleared on drawer close. No history persisted.
Input cleared on send.
---
## Mobile-first layout rules
All layout decisions start at 375px and scale up.
- Bottom navigation: fixed, height 56px, icons + labels
- R42 button: 48px circle, positioned above nav bar
- Session topic cards: full width, stack vertically
- Micro learning components: full width, no horizontal scroll except
ComparisonCard (swipeable)
- Heatmap: horizontal scroll container on narrow screens
- Leaderboard table: horizontally scrollable on mobile, sticky name column
- Drawer/sheet pattern for R42 on mobile, side panel on desktop (breakpoint: 768px)
- Tap targets: minimum 44×44px on all interactive elements
- No hover-only interactions — all hover states have tap equivalents
---
## Environment variables
```
NEXT_PUBLIC_POCKETBASE_URL=http://localhost:8090
NEXT_PUBLIC_INGESTION_URL=http://localhost:3001
NEXT_PUBLIC_GENERATION_URL=http://localhost:3002
NEXT_PUBLIC_CURRICULUM_URL=http://localhost:3003
NEXT_PUBLIC_CHAT_URL=http://localhost:3004
NEXT_PUBLIC_PROGRESS_URL=http://localhost:3005
```
---
## Dependencies
```json
{
"dependencies": {
"next": "14",
"react": "^18",
"react-dom": "^18",
"pocketbase": "^0.21",
"next-pwa": "^5",
"zod": "^3"
},
"devDependencies": {
"typescript": "^5",
"tailwindcss": "^3",
"autoprefixer": "^10",
"postcss": "^8",
"@types/react": "^18",
"@types/node": "^20"
}
}
```
No component library. No charting library. No drag-and-drop library —
implement curriculum drag-to-reorder with native HTML5 drag API.
The heatmap is SVG or CSS grid — no D3.
---
## TypeScript strict mode requirements
- No `any` types
- All PocketBase collection responses typed against data-model.md schemas
- All service API responses typed against response types from each service spec
- Micro learning content JSON typed per type using discriminated union:
```typescript
type MicroLearningContent =
| { type: 'concept_explainer'; paragraphs: string[]; example: string }
| { type: 'scenario_quiz'; scenario: string; options: QuizOption[] }
| { type: 'misconceptions'; items: MisconceptionItem[] }
// ... all 10 types
```
- SSE event types as discriminated union
- No implicit any on event handlers
---
## What the frontend does NOT do
- Does not run AI calls directly — all AI goes through backend services
- Does not write to Qdrant — embedding is the ingestion service's responsibility
- Does not implement auth logic — delegates entirely to PocketBase SDK
- Does not implement curriculum generation — calls curriculum service
---
## Testing checkpoints
### Admin app
1. Upload a PDF → ingestion job created → status polls and updates → done state shows link
2. Theme batch appears after ingestion → approve → generation queued
3. Edit a topic title and body → save → changes persisted in PocketBase
4. Curriculum renders 26 weeks → drag week 3 and week 5 → order persists
5. Regeneration banner appears → preview shows → confirm applies new version
### Employee app
6. Login as employee → redirected to /app/session → correct week shown
7. Select micro learning type → content renders → mark complete → commits toast shown
8. Complete all topics in week → week complete state shown → advance to next week
9. Library browse → search filters results → topic detail renders body + related topics
10. Profile page → heatmap renders for current cycle → badges show locked/unlocked state
11. Leaderboard tab → all employees shown → current employee row highlighted
12. R42 button visible on every screen → opens drawer → question answered with citations
13. R42 citation tap → navigates to correct topic in library
14. Out-of-scope question → muted message shown, no citations
15. All screens render correctly at 375px width — no horizontal overflow except
intentional scroll containers

View File

@@ -1,469 +1,54 @@
# Gamification and progress service spec # Gamification spec
## Responsibility A lightweight points-and-badges layer that motivates weekly completion. There is
no separate progress service — points are written by the test flow and badges are
computed at render time.
Records session completions, calculates XP (commits), manages levels and - **Ledger:** the `leaderboard` collection
streaks, evaluates badge conditions, generates milestone cards, and serves - **Award path:** `src/lib/testService.js → saveTestResult`
leaderboard data. All gamification data is public to all employees. - **Display:** `src/pages/Leaderboard.jsx` (+ the Dashboard mini-leaderboard)
--- ---
## Service location ## Points
``` - **+2 points per correct quiz answer** (`pointsEarned = score * 2`).
app/services/progress/ - Written via `db.upsertLeaderboardEntry(userId, name, pointsDelta, testsCompletedDelta)`,
├── src/ which increments `points` and `tests_completed`.
│ ├── index.ts entry point, Fastify server
│ ├── routes/ `leaderboard` fields: `user_id`, `name`, `points`, `tests_completed`,
│ │ ├── completions.ts POST /complete `learnings_completed` (reserved).
│ │ ├── profile.ts GET /profile/:userId
│ │ ├── leaderboard.ts GET /leaderboard
│ │ └── feed.ts GET /feed
│ ├── engine/
│ │ ├── xp.ts commit calculation per type
│ │ ├── level.ts level thresholds + promotion
│ │ ├── streak.ts streak evaluation
│ │ ├── badges.ts badge condition evaluation
│ │ └── milestone.ts milestone card generation
│ └── lib/
│ └── pocketbase.ts
├── package.json
├── tsconfig.json
└── .env.example
```
--- ---
## API surface ## Badges (computed at render time)
### POST /complete Defined in `Leaderboard.jsx`; not stored in the database:
Called by the frontend when an employee completes a micro learning. | Badge | Condition |
|---|---|
Request: | First Steps | `tests_completed > 0` |
```json | Veteran | `tests_completed >= 5` |
{ | Perfectionist | any test scored 100% (`perfectScores > 0`) |
"userId": "string",
"topicId": "string",
"microLearningId": "string",
"microLearningType": "string",
"weekNumber": 7,
"cycle": 1
}
```
Response:
```json
{
"commitsEarned": 15,
"totalCommits": 342,
"levelBefore": "junior",
"levelAfter": "junior",
"streakWeeks": 5,
"newBadges": [
{ "key": "deep_reader", "label": "Deep reader", "tier": "content" }
],
"milestoneCard": null
}
```
`newBadges` is empty array if no badges earned this completion.
`milestoneCard` is populated only at weeks 13 and 26.
--- ---
### GET /profile/:userId ## Leaderboard
Returns full gamification profile for an employee. - Admins are filtered out of the public board.
- All non-admin members appear, even with 0 points.
Response: - Sorted by `points` descending; top 3 shown as a podium, the rest as a ranked list
```json with their earned badges. The current user is highlighted.
{ - The Dashboard shows a compact top-3 + the viewer's rank.
"userId": "string",
"displayName": "string",
"totalCommits": 342,
"level": "junior",
"currentStreakWeeks": 5,
"longestStreakWeeks": 8,
"typesUsed": ["concept_explainer", "scenario_quiz", "how_to"],
"typesNotUsed": ["case_study", "myth_vs_evidence"],
"badges": [
{ "key": "bronze_1", "label": "First commit", "tier": "bronze", "earnedAt": "..." }
],
"heatmap": [
{ "week": 1, "completions": 3 },
{ "week": 2, "completions": 0 }
]
}
```
Heatmap returns 26 entries per cycle. `completions` = number of micro
learning types completed that week.
--- ---
### GET /leaderboard ## Notes & possible extensions
Returns all employee gamification profiles for leaderboard rendering. - The earlier design described developer-themed XP ("commits"), levels
Not paginated at 150 employees. (Intern→Principal), weekly streaks, a GitHub-style heatmap, and public milestone
cards. **None of those shipped** — the live system is the points + 3-badge model
Response: above.
```json - If extending: add fields to `leaderboard` (or a new collection + migration in
{ `pb_migrations/`, mirrored in `scripts/setup-pb-collections.mjs`), increment them
"employees": [ on the relevant completion events, and surface them in `Leaderboard.jsx`.
{
"userId": "string",
"displayName": "string",
"totalCommits": 847,
"currentStreakWeeks": 18,
"typesUsedCount": 9,
"badgeCount": 5,
"level": "senior"
}
]
}
```
Sorted by totalCommits descending. Frontend renders as multi-dimension
table — not a ranked list.
---
### GET /feed
Returns recent milestone cards for the public activity feed.
Most recent first.
Response:
```json
{
"milestones": [
{
"userId": "string",
"displayName": "string",
"cycle": 1,
"week": 26,
"totalCommits": 847,
"streakWeeks": 18,
"badges": ["on_streak", "shipped"],
"createdAt": "string"
}
]
}
```
---
## XP (commits) calculation
Each micro learning type earns a different number of commits based on
cognitive effort required.
```typescript
const COMMITS_PER_TYPE: Record<MicroLearningType, number> = {
concept_explainer: 10,
glossary_anchor: 10,
misconceptions: 15,
how_to: 15,
flashcard_set: 15,
comparison_card: 20,
reflection_prompt: 20,
scenario_quiz: 25,
myth_vs_evidence: 25,
case_study: 30,
}
```
First completion of a type the employee has never used before: +5 bonus
commits (rewards type diversity).
Duplicate completion (same topic + same type in same cycle): 0 commits.
Check session_completions before awarding — idempotent.
---
## Levels
Thresholds based on cumulative commits across all cycles.
```typescript
const LEVEL_THRESHOLDS = {
intern: 0,
junior: 100,
medior: 300,
senior: 600,
staff: 1000,
principal: 1500,
}
```
Level evaluated after every commit update. Level can only increase —
never decreases between cycles.
---
## Streak
Counts consecutive weeks with at least one completion.
Evaluation logic on every POST /complete:
```typescript
const lastActiveWeek = profile.last_active_week
const currentWeek = completionWeekNumber
if (currentWeek === lastActiveWeek) {
// Same week — streak unchanged
} else if (currentWeek === lastActiveWeek + 1) {
// Consecutive — increment
streak += 1
} else {
// Gap — reset to 1
streak = 1
}
profile.last_active_week = currentWeek
profile.longest_streak_weeks = Math.max(streak, profile.longest_streak_weeks)
```
Week number resets to 1 on cycle start. Streak does not reset on cycle
transition — a streak spanning the cycle boundary is maintained.
---
## Badges
### Badge seed data
Seeded into PocketBase badges collection at startup.
```typescript
const BADGE_DEFINITIONS = [
{ key: 'first_commit', tier: 'bronze',
label: 'First commit',
description: 'Complete any session' },
{ key: 'five_sessions', tier: 'silver',
label: 'Five sessions',
description: 'Complete 5 sessions using 5 different micro learning types' },
{ key: 'on_streak', tier: 'gold',
label: 'On a streak',
description: 'Complete 13 sessions without skipping a week' },
{ key: 'shipped', tier: 'legendary',
label: 'Shipped',
description: 'Complete all 26 sessions using all 10 micro learning types' },
{ key: 'governance_nerd', tier: 'content',
label: 'Governance nerd',
description: 'Complete all topics in the holacratic structure theme' },
{ key: 'process_architect', tier: 'content',
label: 'Process architect',
description: 'Complete all topics in the internal processes theme' },
{ key: 'deep_reader', tier: 'content',
label: 'Deep reader',
description: 'Use the case study micro learning type 5 or more times' },
{ key: 'handbook_expert', tier: 'content',
label: 'Handbook expert',
description: 'Complete all topics in the employee handbook theme' },
{ key: 'type_collector', tier: 'content',
label: 'Type collector',
description: 'Use all 10 micro learning types at least once' },
]
```
Content badges are theme-specific. Theme association resolved at runtime
by matching badge key to theme title pattern — not hardcoded to theme IDs.
```typescript
const THEME_BADGE_PATTERNS: Record<string, string> = {
'governance_nerd': 'holacrat',
'process_architect': 'process',
'handbook_expert': 'handbook',
}
```
Case-insensitive substring match on theme title.
---
### Badge evaluation
Run after every POST /complete. Check all conditions, award unearned badges.
```typescript
async function evaluateBadges(userId: string, profile: GamificationProfile):
Promise<BadgeDefinition[]> {
const earnedKeys = await getEarnedBadgeKeys(userId)
const newBadges: string[] = []
if (!earnedKeys.includes('first_commit')) {
const count = await countCompletions(userId)
if (count >= 1) newBadges.push('first_commit')
}
if (!earnedKeys.includes('five_sessions')) {
const sessions = await countUniqueSessions(userId)
if (sessions >= 5 && profile.types_used.length >= 5)
newBadges.push('five_sessions')
}
if (!earnedKeys.includes('on_streak')) {
if (profile.longest_streak_weeks >= 13) newBadges.push('on_streak')
}
if (!earnedKeys.includes('shipped')) {
const cycleComplete = await isCycleComplete(userId, profile.current_cycle)
if (cycleComplete && profile.types_used.length === 10)
newBadges.push('shipped')
}
if (!earnedKeys.includes('deep_reader')) {
const count = await countTypeCompletions(userId, 'case_study')
if (count >= 5) newBadges.push('deep_reader')
}
if (!earnedKeys.includes('type_collector')) {
if (profile.types_used.length === 10) newBadges.push('type_collector')
}
for (const [badgeKey, pattern] of Object.entries(THEME_BADGE_PATTERNS)) {
if (!earnedKeys.includes(badgeKey)) {
const complete = await isThemeComplete(userId, pattern)
if (complete) newBadges.push(badgeKey)
}
}
for (const key of newBadges) {
await awardBadge(userId, key, profile.current_cycle)
}
return newBadges.map(k => BADGE_DEFINITIONS.find(b => b.key === k)!)
}
```
---
## Milestone cards
Generated at weeks 13 and 26 of each cycle.
```typescript
async function generateMilestoneCard(
userId: string,
weekNumber: number,
cycle: number,
profile: GamificationProfile
): Promise<MilestoneCard | null> {
if (weekNumber !== 13 && weekNumber !== 26) return null
const exists = await milestoneExists(userId, cycle, weekNumber)
if (exists) return null
const badges = await getEarnedBadges(userId)
return await pocketbase.collection('milestone_cards').create({
user: userId,
cycle,
week: weekNumber,
total_commits: profile.total_commits,
streak_weeks: profile.current_streak_weeks,
badge_keys: badges.map(b => b.key),
created_at: new Date().toISOString()
})
}
```
---
## Heatmap data
Built from session_completions filtered by userId + cycle.
Returns 26 entries — one per week.
```typescript
async function buildHeatmap(userId: string, cycle: number):
Promise<HeatmapEntry[]> {
const completions = await pocketbase
.collection('session_completions')
.getFullList({ filter: `user="${userId}" && cycle=${cycle}` })
const byWeek = groupBy(completions, c => c.week_number)
return Array.from({ length: 26 }, (_, i) => ({
week: i + 1,
completions: byWeek[i + 1]?.length ?? 0
}))
}
```
---
## Environment variables
```
POCKETBASE_URL=
POCKETBASE_ADMIN_EMAIL=
POCKETBASE_ADMIN_PASSWORD=
PROGRESS_PORT=3005
```
---
## Dependencies
```json
{
"dependencies": {
"fastify": "^4",
"pocketbase": "^0.21",
"zod": "^3"
}
}
```
No AI calls. No Qdrant. No OpenAI. Pure business logic over PocketBase.
---
## TypeScript strict mode requirements
- No `any` types
- MicroLearningType as explicit string union
- Badge keys as explicit string union matching BADGE_DEFINITIONS
- COMMITS_PER_TYPE keyed by MicroLearningType — compile-time exhaustiveness
- HeatmapEntry, MilestoneCard, BadgeDefinition typed explicitly
---
## What this service does NOT do
- Does not generate content
- Does not handle curriculum scheduling → curriculum service
- Does not serve KB or micro learning data → frontend reads PocketBase
- Does not handle auth → PocketBase + frontend
---
## Testing checkpoints
1. POST /complete for new user → first_commit badge awarded, commits added
2. POST /complete same topic + type twice → 0 commits second call (idempotent)
3. Complete 5 sessions with 5 types → five_sessions badge awarded
4. Simulate 13 consecutive weekly completions → on_streak badge awarded
5. Skip a week → streak resets to 1
6. Complete all topics in a theme → content badge awarded
7. Use all 10 types → type_collector badge awarded
8. Complete week 13 → milestone_card created and returned in response
9. GET /leaderboard → all employees returned with correct fields
10. GET /feed → milestone cards ordered most recent first
11. Cycle transition: week 26 complete → streak spans boundary, level preserved,
heatmap resets for new cycle

View File

@@ -1,583 +1,87 @@
# Generation service spec # Generation spec: learning content & micro-learnings
## Responsibility Two generators turn a topic into learner-facing material. Both go through
`callLLM` with forced tool use and Zod-validated output. All content is cached in
Accepts a Theme ID from the admin app (on batch approval) and generates all 10 PocketBase so it is generated once per topic/type.
micro learning types for every published Topic in that Theme. One Claude Sonnet 4
call per type per topic. All outputs validated through Zod schemas before write.
This service runs entirely server-side. The admin app calls it via REST. All AI
calls go through the Anthropic API. No generation logic lives in the frontend.
--- ---
## Service location ## A. Long-form content — `src/lib/learningService.js`
``` Stored in the `content` collection (one record per topic, `data` is a merged
app/services/generation/ object). Three types, generated **on demand**:
├── src/
│ ├── index.ts entry point, Fastify server | Type | Tool | Min requirements |
│ ├── routes/ |---|---|---|
│ │ ├── generate.ts POST /generate, GET /status/:jobId | `article` | `emit_learning_article` | ≥3 sections, ≥2 takeaways |
│ │ └── publish.ts PATCH /micro-learnings/:id | `slides` | `emit_learning_slides` | ≥4 slides |
│ ├── pipeline/ | `infographic` | `emit_learning_infographic` | ≥3 stats, ≥3 steps |
│ │ └── generate.ts per-type generation logic
│ ├── jobs/ `generateLearningContent(topic, force, selectedType)`:
│ │ └── queue.ts async job queue (in-memory) - tier `standard`, `maxTokens: 8192`
│ ├── lib/ - `selectedType` is one of the three, or `'all'` (`emit_learning_all`) for admin regeneration
│ │ ├── pocketbase.ts PocketBase client - cache check looks at `content[selectedType]`; on generation the new payload is
│ │ └── anthropic.ts Anthropic client **shallow-merged** into the cached object so other types survive
│ └── types.ts shared TypeScript types + Zod schemas - there is **no podcast type**
├── package.json
├── tsconfig.json **Article refinement** (`refineLearningContent`): the admin describes a change and
├── .env.example the model edits via targeted patch tools — `set_intro`, `set_section`,
└── .gitignore `add_section`, `remove_section`, `replace_takeaways` — so only the affected parts
``` change. Patches are applied and re-validated in `src/lib/articlePatches.js`.
--- ---
## API surface ## B. Micro-learnings — `src/lib/microLearningService.js`
### POST /generate Stored in the `micro_learnings` collection (one record per topic per type,
`status='published'`). Three types:
Triggered by admin app when a Theme batch is approved. | Type | Tool | Tier | Shape |
|---|---|---|---|
| `concept_explainer` | `emit_concept_explainer` | standard | `{ sections: [{ title, content (HTML) }] }`, ≥3 sections |
| `scenario_quiz` | `emit_scenario_quiz` | standard | `{ scenario, options: [{ text, isCorrect, explanation }] }`, 34 options, exactly 1 correct |
| `flashcard_set` | `emit_flashcard_set` | fast (Haiku) | `{ cards: [{ front, back }] }`, 510 cards |
Request: `getOrGenerateMicroLearning(topicId, type)`:
```json - returns the cached published record if one exists (`findExisting`)
{ - otherwise loads the topic, calls `callLLM` with forced tool choice, and creates a
"themeId": "string" `micro_learnings` record with the validated `content`
}
```
Response (202 Accepted): > A former `reflection_prompt` type was dropped. Do not re-add it.
```json
{
"jobId": "string",
"status": "queued",
"topicsFound": 5,
"totalItems": 50
}
```
Processing is async. The admin app polls job status. Completion is recorded (append-only) by `useMicroLearningCompletions` into
`micro_learning_completions` with `{ team_member_id, micro_learning_id, topic_id,
Behaviour: type, session_week }`.
- Fetches all published Topics for the given themeId
- Creates one micro_learnings record per topic per type with status `queued`
- Generates each item sequentially; updates status to `generated` on success
- On failure: sets individual item status to `failed`, continues remaining items
- Job completes when all items are either `generated` or `failed`
--- ---
### GET /status/:jobId ## C. Weekly quiz — `src/lib/testService.js`
Returns current job progress. Generates a 5-question multiple-choice test for the user's current week.
Response: - **Topic selection** (`selectTestTopics`): primary topic from the active
```json curriculum week (else hash fallback) + a few review topics for breadth.
{ - **Batch generation** (`callQuizBatchModel`): a single `fast`-tier call
"jobId": "string", (`emit_quiz_questions`, `maxTokens: 4096`, 25s timeout) returns all 5 questions.
"status": "queued" | "running" | "done" | "failed", - **Quality gates** (`validateBatchQuality`): no duplicate options; no banned
"progress": { fillers ("all/none of the above", "both A and B"); explanations ≥20 chars; reject
"topicsTotal": 5, if `correctIndex` is dominated by one position (>80%) and re-roll.
"topicsProcessed": 3, - **Scoring** (`saveTestResult`): `pointsEarned = score * 2`, written to
"itemsTotal": 50, `leaderboard` via `db.upsertLeaderboardEntry`.
"itemsGenerated": 28,
"itemsFailed": 2 Question shape: `{ id, question, topicLabel, options[4], correctIndex (03),
}, explanation, difficulty }`.
"error": "string | null"
}
```
--- ---
### PATCH /micro-learnings/:id ## Shared infrastructure (`src/lib/llm.js`)
Admin publishes or rejects an individual micro learning. - **Tiers:** `fast` (Haiku 4.5), `standard` (Sonnet 4.6), `reasoning` (Opus 4.7);
per-tier admin overrides via `admin:model:{tier}`.
Request: - **Structured output:** prefer tool use with forced `toolChoice`; inputs validated
```json by `toolSchemaRegistry`. Text responses go through `parseStructuredText`.
{ - **Caching:** wrap stable system text with `cachedSystem(...)`.
"status": "published" | "rejected" - **Retry/limits:** `src/lib/llmRetry.js` — backoff + jitter on 408/425/429/5xx/529,
} honors `Retry-After`, rate limiters for bulk work.
``` - **Telemetry:** every call logged to `llm_calls`.
- **Simulation:** with `admin:use_simulation`, calls return stub output (no API hit).
Response (200 OK):
```json
{
"id": "string",
"status": "published" | "rejected",
"published_at": "datetime | null"
}
```
Rules:
- Only `generated` records can be published or rejected
- `published_at` set on publish, left null on reject
- Returns 400 if record is not in `generated` status
- Returns 404 if record not found
---
## Generation pipeline
### Input
For each Topic in the approved Theme:
```
topic.title: string
topic.body: string
topic.key_terms: string[]
topic.difficulty: 'introductory' | 'intermediate' | 'advanced'
```
### Output
10 micro_learnings records per topic, one per type.
---
## AI call configuration
```typescript
{
model: 'claude-sonnet-4-20250514',
max_tokens: 2000,
temperature: 0 // deterministic structured output
}
```
One call per type per topic. Do not batch multiple types into one call — isolated
calls are easier to retry and validate independently.
---
## Prompt strategy
### System prompt (all types)
```
You are a learning content designer. Your task is to generate structured learning
content for a specific topic in an employee learning platform.
Output ONLY valid JSON matching the schema provided. No preamble, no explanation,
no markdown fences.
The content should be accurate, practical, and appropriate for the stated
difficulty level. Tone: professional but accessible.
```
### User prompt template (all types)
```
Topic: {topic.title}
Difficulty: {topic.difficulty}
Body:
{topic.body}
Key terms: {topic.key_terms.join(', ')}
Generate a {type_label} for this topic.
Output schema:
{JSON.stringify(schemaDescription)}
```
---
## Type-specific prompts and schemas
### concept_explainer
Type label: `Concept Explainer`
Schema description:
```json
{
"paragraphs": ["2 to 3 paragraphs explaining the concept in plain language"],
"example": "one concrete real-world example"
}
```
Zod schema:
```typescript
z.object({
paragraphs: z.array(z.string()).min(2).max(3),
example: z.string().min(20)
})
```
---
### scenario_quiz
Type label: `Scenario Quiz`
Schema description:
```json
{
"scenario": "a realistic workplace scenario",
"options": [
{ "label": "A", "text": "answer text", "correct": false, "explanation": "why" },
{ "label": "B", "text": "answer text", "correct": true, "explanation": "why" },
{ "label": "C", "text": "answer text", "correct": false, "explanation": "why" },
{ "label": "D", "text": "answer text", "correct": false, "explanation": "why" }
]
}
```
Rules: exactly 4 options, exactly 1 correct.
Zod schema:
```typescript
z.object({
scenario: z.string().min(30),
options: z.array(z.object({
label: z.enum(['A', 'B', 'C', 'D']),
text: z.string().min(5),
correct: z.boolean(),
explanation: z.string().min(10)
})).length(4).refine(
opts => opts.filter(o => o.correct).length === 1,
{ message: 'exactly one correct option required' }
)
})
```
---
### misconceptions
Type label: `Misconceptions`
Schema description:
```json
{
"items": [
{ "misconception": "common wrong belief", "correction": "accurate explanation" }
]
}
```
Rules: 3 to 5 items.
Zod schema:
```typescript
z.object({
items: z.array(z.object({
misconception: z.string().min(10),
correction: z.string().min(10)
})).min(3).max(5)
})
```
---
### how_to
Type label: `How-To Guide`
Schema description:
```json
{
"steps": [
{ "number": 1, "instruction": "what to do" }
]
}
```
Rules: 3 to 8 steps.
Zod schema:
```typescript
z.object({
steps: z.array(z.object({
number: z.number().int().positive(),
instruction: z.string().min(10)
})).min(3).max(8)
})
```
---
### comparison_card
Type label: `Comparison Card`
Schema description:
```json
{
"subject_a": "first concept or approach",
"subject_b": "second concept or approach",
"dimensions": [
{ "label": "dimension name", "a": "how A differs", "b": "how B differs" }
]
}
```
Rules: 3 to 6 dimensions.
Zod schema:
```typescript
z.object({
subject_a: z.string().min(2),
subject_b: z.string().min(2),
dimensions: z.array(z.object({
label: z.string().min(2),
a: z.string().min(5),
b: z.string().min(5)
})).min(3).max(6)
})
```
---
### reflection_prompt
Type label: `Reflection Prompt`
Schema description:
```json
{
"prompt": "open-ended question for the employee to reflect on",
"model_answer": "a thoughtful example answer the employee can compare against"
}
```
Zod schema:
```typescript
z.object({
prompt: z.string().min(20),
model_answer: z.string().min(50)
})
```
---
### flashcard_set
Type label: `Flashcard Set`
Schema description:
```json
{
"cards": [
{ "question": "question text", "answer": "answer text" }
]
}
```
Rules: 5 to 10 cards.
Zod schema:
```typescript
z.object({
cards: z.array(z.object({
question: z.string().min(5),
answer: z.string().min(5)
})).min(5).max(10)
})
```
---
### case_study
Type label: `Case Study`
Schema description:
```json
{
"scenario": "a detailed real-world scenario (150+ words)",
"questions": ["discussion or reflection question 1", "discussion or reflection question 2"]
}
```
Rules: 2 to 4 questions.
Zod schema:
```typescript
z.object({
scenario: z.string().min(150),
questions: z.array(z.string().min(10)).min(2).max(4)
})
```
---
### glossary_anchor
Type label: `Glossary Anchor`
Schema description:
```json
{
"term": "the key term",
"definition": "precise definition",
"correct_use": "example sentence showing correct use",
"misuse": "common incorrect usage to avoid"
}
```
Prompt addition: use the first key term from `topic.key_terms` as the anchor term.
Zod schema:
```typescript
z.object({
term: z.string().min(2),
definition: z.string().min(20),
correct_use: z.string().min(20),
misuse: z.string().min(20)
})
```
---
### myth_vs_evidence
Type label: `Myth vs Evidence`
Schema description:
```json
{
"myth": "a commonly held misconception about this topic",
"evidence": "the evidence-based counterpoint",
"sources": ["source or reference if applicable — leave empty array if none"]
}
```
Zod schema:
```typescript
z.object({
myth: z.string().min(20),
evidence: z.string().min(30),
sources: z.array(z.string())
})
```
---
## Error handling
**Per item:**
- JSON parse failure → retry once with stricter prompt ("respond with valid JSON only, no other text")
- Second failure → set micro_learning status to `failed`, log raw response, continue to next item
- Zod validation failure → same as parse failure: retry once, then `failed`
- Anthropic API error (rate limit / timeout) → exponential backoff, 3 retries, then `failed`
**Per job:**
- If all items for a topic fail → log, continue to next topic
- Job status becomes `done` when all items processed, regardless of individual failures
- Job status becomes `failed` only if the initial topic fetch fails (PocketBase error before generation starts)
---
## PocketBase write
For each generated item:
```typescript
{
topic: topicId,
type: type, // one of the 10 type enum values
content: validatedContent, // JSON, validated by Zod
status: 'generated',
generation_model: 'claude-sonnet-4-20250514',
generated_at: new Date().toISOString()
}
```
Create record with status `queued` before generation starts.
Update to `generated` (with content) or `failed` after attempt.
---
## Job lifecycle
```
POST /generate received
Fetch published Topics for Theme
Create micro_learning records: status = queued
Job created → status: running
For each topic:
For each of 10 types:
Claude call → validate → write content → status = generated
All items processed
Job status: done
```
On topic fetch failure:
```
status: failed
error: { reason: 'topic_fetch_failed', detail: ... }
```
---
## Environment variables required
```
ANTHROPIC_API_KEY=
POCKETBASE_URL=
POCKETBASE_ADMIN_EMAIL=
POCKETBASE_ADMIN_PASSWORD=
GENERATION_PORT=3002
```
---
## Dependencies
```json
{
"dependencies": {
"fastify": "^4",
"@anthropic-ai/sdk": "^0.24",
"pocketbase": "^0.21",
"uuid": "^9",
"zod": "^3"
},
"devDependencies": {
"typescript": "^5",
"@types/node": "^20",
"tsx": "^4"
}
}
```
---
## TypeScript strict mode requirements
- No `any` types
- All Claude response parsing through Zod schema validation before PocketBase write
- All PocketBase writes typed against micro_learnings schema from data-model.md
- Content type is `unknown` after JSON.parse — always validate through Zod before use
---
## What this service does NOT do
- Does not extract or chunk source documents → ingestion service
- Does not build or schedule the curriculum → curriculum service
- Does not handle admin auth → PocketBase + admin app
- Does not embed content into Qdrant → ingestion service handles all embeddings
- Does not serve R42 queries → chat service
---
## Testing checkpoints
1. Call POST /generate with a themeId that has 2 published topics → verify 20 micro_learning records created
2. All 10 types generated for each topic → verify content JSON parses correctly
3. All Zod schemas pass for each of the 10 types
4. PATCH /micro-learnings/:id with `published` → verify status + published_at updated
5. PATCH /micro-learnings/:id with `rejected` → verify status updated, published_at null
6. Force a JSON parse error (mock) → verify retry logic fires once, then sets status to `failed`
7. GET /status/:jobId during processing → verify progress counters increment correctly

View File

@@ -1,231 +1,114 @@
# Handover: employee learning platform # Handover: Respellion Learning Platform
## Purpose of this document ## Purpose of this document
This document captures every design decision made before implementation started. This document captures the **design decisions as actually built**. The platform
It is the authoritative source for rationale. When a spec file is ambiguous, diverged substantially from its original design vision (a Next.js multi-service
resolve it against this document. Do not ask the human — the answers are here. system with Qdrant and OpenAI embeddings). This handover reflects what shipped:
a React/Vite SPA on PocketBase with local TF-IDF retrieval.
When sources conflict, trust the code in `src/` first, then this document, then
`docs/data-model.md` (schema), then `docs/architecture.md`.
--- ---
## What this application does ## What this application does
Employees of a tech company use this platform to build and maintain knowledge of Employees use the platform to build and maintain knowledge of the company's
the employee handbook, holacratic structures, and internal processes. internal handbook, roles, and processes.
Core mechanics: Core mechanics:
- Admins upload source documents → AI extracts a structured knowledge base - Admins upload source documents → Claude extracts a structured knowledge graph (topics + relations)
- The KB is organised into Themes (broad) and Topics (specific) - AI generates learning content and micro-learnings per topic
- An AI generates 10 types of micro learning content per Topic - Each employee follows a 26-week curriculum, **starting whenever they enroll**
- Employees follow a 26-week curriculum of weekly sessions - Each week presents an assigned topic; the employee completes micro-learnings and a test
- Each session covers one Theme (multiple related Topics) - After week 26 the cycle restarts at week 1 with the same content
- Employees choose which micro learning type to use per Topic - R42, an AI assistant, answers KB-grounded questions on every screen
- After 26 weeks the curriculum restarts, varied to reinforce rather than repeat - A gamification layer (points, badges, leaderboard) motivates completion
- A chatbot called R42 answers KB-grounded questions on every screen
- A gamification system using developer-native language motivates completion
--- ---
## All confirmed design decisions ## Key decisions as built
### Architecture
- **Single-page React app, not microservices.** All logic runs in the browser
(`src/`). PocketBase is the only backend; the Anthropic API is reached through a
reverse proxy (Caddy in prod, Vite in dev). The original `app/` Next.js scaffold
was abandoned and is not deployed.
- **PocketBase for everything stateful** — auth, structured data, file storage.
SQLite is sufficient at this scale.
- **No vector database.** Retrieval is a dependency-free TF-IDF index over the
knowledge graph (`src/lib/retrieval.js`). Qdrant and the embedding service from
the original design were never built.
### Knowledge base ### Knowledge base
- **Extracted, not hand-authored.** Admins upload `.txt` / `.md` (≤5 MB). Claude
(standard tier) extracts topics and relations chunk by chunk.
- **Flat graph, not a Theme→Topic tree.** The KB is `topics` + `relations`. A
topic's `theme` is a string used for curriculum grouping, not a separate entity.
- **Relation types:** `related_to`, `depends_on`, `part_of`, `executed_by`.
- **Topic relevance** (`core` / `standard` / `peripheral` / `exclude`) controls
what enters learning/curriculum; `relevance_locked` protects admin overrides on
re-ingestion.
**Decision: KB is extracted from source documents, not manually authored** ### Learning content
Admins upload raw source material. Claude Sonnet 4 extracts Themes, Topics, and - **Long-form content is generated on demand**, three types: `article`, `slides`,
relationships. Admins review and approve in batches (one Theme at a time, not `infographic` (the `content` collection). New types shallow-merge into the cached
one Topic at a time). Topic bodies are AI-drafted and admin-editable after approval. object. **No podcast type.**
- **Micro-learnings**, three types: `concept_explainer`, `scenario_quiz`,
**Decision: two-level hierarchy — Theme → Topic** `flashcard_set` (the `micro_learnings` collection). A former `reflection_prompt`
A Theme is a broad subject area. A Topic is one specific concept within a Theme. type was dropped.
One weekly session = one Theme. Multiple Topics within that Theme per session. - **Employee chooses the format** per topic per session. Completion is not
quality-gated; engaging with the full micro-learning counts.
**Decision: three relationship types between Topics**
- related: Topics that complement each other
- prerequisite: Topic A should be understood before Topic B
- contrast: Topics representing opposing approaches
These relationships are stored as explicit PocketBase relation fields, not a
generic junction table.
**Decision: source material format priority**
Accepted formats: PDF, MD, TXT only. MD is the highest quality input —
heading structure maps directly to Theme → Topic hierarchy. Admins should be
recommended to provide MD where possible.
**Decision: embeddings from source chunks, not topic summaries only**
R42 retrieves from original source material chunks as primary source, with
topic summaries as secondary. This keeps R42 grounded and reduces hallucination.
---
### Micro learnings
**Decision: 10 types, all generated by AI as structured JSON**
Types:
1. concept_explainer
2. scenario_quiz
3. misconceptions
4. how_to
5. comparison_card
6. reflection_prompt
7. flashcard_set
8. case_study
9. glossary_anchor
10. myth_vs_evidence
Each type has a defined JSON schema in data-model.md. Generation uses
Claude Sonnet 4. Output is validated against Zod schemas before storage.
**Decision: employees choose type per topic per session**
Employees are not locked to one type globally. Each session, per Topic, the
employee selects from all published types for that topic. Multiple types can
be completed in one session.
**Decision: pre-generate, don't generate on demand**
All 10 types are generated when a Topic is approved, not when an employee
requests them. This controls quality and cost. R42 is the only on-demand
AI interaction.
---
### Curriculum ### Curriculum
- **AI generates, admin confirms.** Claude proposes a 26-week schedule from the
**Decision: AI generates curriculum, admin edits** themed/weighted topic set; the admin previews and activates it. Versions move
Claude Sonnet 4 reads the full KB graph (Themes, Topics, relationships, `draft → active → superseded`; exactly one is active.
complexity weights) and produces a 26-week schedule. Admin reviews, reorders, - **Per-user, self-paced start (current behavior).** Each employee enrolls on first
and finetunes. Admin does not build from scratch. login; their week/cycle is derived from `curriculum_started_at`. There is **no
shared calendar week**. Week 1 is the first 7 days after they enroll.
**Decision: one Theme per week session** - **Perpetual, repeating cycles.** After week 26, the cycle restarts at week 1 with
A session covers all Topics under one Theme. Topics within the session are the same content. Completion history (`micro_learning_completions`) is append-only.
ordered by the curriculum generator based on prerequisites and complexity. - **Hash fallback.** If no curriculum version is active, topic assignment falls back
to a deterministic hash of user id + week. Keep this fallback.
**Decision: perpetual curriculum with versioning**
The curriculum runs indefinitely. After week 26, cycle 2 begins on the latest
curriculum version. Cycle 2+ varies sequence, surfaces unused micro learning
types, and increases coverage of low-engagement topics.
**Decision: completed weeks are immutable**
Regeneration only affects future unstarted weeks. An employee's completion
history is never altered regardless of curriculum version changes.
**Decision: regeneration requires admin confirmation**
When new Topics are approved, the system queues a regeneration but does not
apply it until the admin explicitly confirms. Admin sees a preview of the
proposed new schedule before confirming.
**Decision: rolling starts**
Each employee has their own start date. There are no cohorts or shared
start dates.
---
### Gamification
**Decision: developer-native visual language**
Inspired by GitHub (heatmap), Stack Overflow (badges, reputation), and
Duolingo (streak, XP, levels). Language uses developer vocabulary throughout.
**Decision: XP unit is called commits**
Every completed Topic earns commits. Quantity varies by micro learning type.
**Decision: levels use developer rank names**
Intern → Junior → Medior → Senior → Staff → Principal
Based on cumulative commits across all cycles.
**Decision: streak is weekly, not daily**
Consecutive weeks with at least one completion. Resets on a skipped week.
**Decision: activity heatmap covers 26-week cycle**
GitHub-style contribution graph. Cell darkness = number of types completed
that week.
**Decision: no social layer**
No comments, reactions, or direct messaging. Gamification is visible but
not interactive between employees.
**Decision: public milestone cards, not ranked leaderboard**
At weeks 13 and 26, a public card is posted to the shared activity feed.
Language: "shipped", not "graduated". The leaderboard shows multiple
dimensions (commits, streak, types used, badges) — not a single ranking.
**Decision: named content badges**
Examples: governance_nerd, process_architect, deep_reader. These are seeded
at startup, not user-generated. See data-model.md for badge schema.
---
### R42 chatbot ### R42 chatbot
- **KB-grounded via TF-IDF**, not vector search. Context = top-K topics + verbatim
mentions + filtered relations + limited deep content.
- **Conversation persists per user** in `localStorage` (cap 50 messages; ~12 turns
sent to the API). It is not stored server-side.
- **Can propose graph edits** (`propose_graph_delta`, ≤3 topics / ≤5 relations).
Admins apply immediately; non-admins queue a suggestion for admin approval.
- **Hidden during quizzes** to protect test integrity.
**Decision: functional only, no personality** ### Gamification
R42 answers questions grounded in the KB. It does not have a defined persona, - **Points:** +2 per correct quiz answer, in the `leaderboard` collection.
tone, or name story beyond the label R42. - **Badges** computed at render time: First Steps (1 test), Veteran (5 tests),
Perfectionist (a 100% score).
- Admins are excluded from the public leaderboard.
**Decision: stateless per session** ### Auth & infrastructure
No chat history is persisted between sessions. This avoids privacy complexity - **PIN auth** against `team_members`; the session id lives in `sessionStorage`.
and keeps the implementation simple. Role `admin` unlocks the Admin panel.
- **Claude model tiers:** `fast` = Haiku 4.5, `standard` = Sonnet 4.6,
**Decision: internal KB scope only** `reasoning` = Opus 4.7. Admins can override per tier from Settings.
R42 cannot search external sources. If a question cannot be answered from the - **Simulation mode** (`admin:use_simulation`) returns stub LLM output for UI work.
KB, R42 says so explicitly. - **Deploy:** Docker image (Caddy serving the built SPA) + PocketBase container;
Ansible playbooks under `infra/` for dev and prod.
**Decision: context-weighted retrieval**
R42 knows the employee's current curriculum week. Retrieval boosts chunks
from the current week's Theme. General KB questions are not restricted.
**Decision: always cites source Topic**
Every R42 response includes the Topic title(s) its answer draws from.
**Decision: Haiku 4.5 for R42, Sonnet 4 for generation**
Low latency matters for chat. The retrieval layer compensates for Haiku's
smaller knowledge base. Sonnet 4 is reserved for generation tasks where
structure and quality matter more than speed.
--- ---
### Infrastructure ## Notable divergences from the original vision
**Decision: PocketBase as primary backend** | Original design (not built) | What shipped |
Auth, file storage, structured data, and admin UI in one binary. SQLite is |---|---|
sufficient for ~150 users. No PostgreSQL needed at this scale. | Next.js 14 PWA + 6 Fastify services | Single React/Vite SPA, no backend services |
| Qdrant + OpenAI embeddings | Local TF-IDF, no embeddings |
| Theme/Topic entity hierarchy, batch approval | Flat `topics` + `relations` graph |
| 10 micro-learning types | 3 micro-learning types |
| `employee_curriculum_state`, `badges`, `milestone_cards`, etc. | `team_members` fields + `leaderboard` + render-time badges |
| Shared calendar-week curriculum | Per-user start, self-paced |
**Decision: Qdrant for vector storage** The abandoned scaffolding for the original design still exists under `/app` — it is
Separate Docker container. Keeps vector operations out of SQLite. not part of the running system.
pgvector was rejected — adding Postgres just for vectors is unnecessary overhead.
**Decision: Next.js 14 PWA for frontend**
Single codebase for admin and employee app. PWA covers mobile without a native
app. Learning platforms do not require native device APIs.
**Decision: five discrete backend services**
Ingestion, generation, curriculum, chat, progress. Each is a separate Fastify
service with its own port and responsibility. They do not call each other
directly — they read/write shared PocketBase collections.
**Decision: PDF parsing starts with pdf-parse (Node.js)**
Switch to pdfplumber Python sidecar only if pdf-parse quality is insufficient
for actual source documents. Do not over-engineer the extraction layer upfront.
---
## What is not yet specced
The following spec files still need to be written before their phases begin:
- /docs/generation-spec.md — micro learning generation service
- /docs/curriculum-spec.md — curriculum generator + versioning
- /docs/r42-spec.md — chat service
- /docs/gamification-spec.md — progress service + gamification mechanics
- /docs/frontend-spec.md — employee app, admin app, PWA config
Do not begin a phase without its spec file. Flag the gap if you reach it.
---
## Source of truth hierarchy
When sources conflict, resolve in this order:
1. This handover document (rationale and decisions)
2. The relevant spec file (implementation detail)
3. data-model.md (schema is authoritative)
4. architecture.md (system structure)
Do not use legacy/ code as a source of truth for anything.

View File

@@ -1,394 +1,62 @@
# Implementation plan # Implementation status & maintenance guide
## How to use this document The platform is **fully implemented and deployed**. This document replaces the
original phased build plan (which targeted a Next.js + Qdrant architecture that was
Work through phases in order. Do not start phase N+1 before phase N passes never shipped). It describes what exists today and where to work.
all acceptance criteria. Each phase lists the spec file to read, the steps
to execute, and the criteria that define done.
At the start of each session: state the phase and step.
At the end of each session: state completed steps and next starting point.
--- ---
## Phase 1 — Infrastructure + ingestion service ## Build status
**Spec to read:** /docs/ingestion-spec.md, /docs/data-model.md | Area | Status | Where it lives |
|---|---|---|
| Auth & onboarding | ✅ shipped | `src/store/AppContext.jsx`, `src/pages/Login.jsx`, `src/pages/Onboarding.jsx` |
| Knowledge ingestion | ✅ shipped | `src/lib/extractionPipeline.js`, `src/components/admin/UploadZone.jsx` |
| Knowledge graph (view/edit) | ✅ shipped | `src/components/admin/KnowledgeGraph.jsx` (D3) |
| Learning content generation | ✅ shipped | `src/lib/learningService.js` (article/slides/infographic) |
| Micro-learnings | ✅ shipped | `src/lib/microLearningService.js`, `src/components/micro_learning/` |
| Weekly test | ✅ shipped | `src/lib/testService.js`, `src/pages/Testen.jsx` |
| Curriculum (26-week, per-user) | ✅ shipped | `src/lib/curriculumService.js`, `src/components/admin/CurriculumManager.jsx` |
| R42 chatbot | ✅ shipped | `src/components/chat/`, `src/lib/kbStore.js`, `src/lib/retrieval.js` |
| Gamification | ✅ shipped | `src/pages/Leaderboard.jsx`, `leaderboard` collection |
| Admin panel | ✅ shipped | `src/pages/Admin/index.jsx` (+ `src/components/admin/`) |
| AI wrapper (tiers/retry/telemetry) | ✅ shipped | `src/lib/llm.js`, `src/lib/llmRetry.js`, `src/lib/llmSchemas.js`, `src/lib/llmTools.js` |
| Deployment (Docker/Caddy/Ansible) | ✅ shipped | `Dockerfile`, `Caddyfile`, `docker-compose.yml`, `infra/` |
### Steps ---
**1.1 — Repo scaffold** ## Where to make changes
```
app/ - **New PocketBase field/collection:** add a migration in `pb_migrations/`
frontend/ (empty, Next.js init comes in phase 4) (follow the existing JS migration style) and mirror it in
services/ `scripts/setup-pb-collections.mjs`. Then add async helpers in `src/lib/db.js`.
ingestion/ - **New AI capability:** add a tool in `src/lib/llmTools.js`, a Zod schema in
generation/ (empty placeholder) `src/lib/llmSchemas.js` (register it in `toolSchemaRegistry`), and call it through
curriculum/ (empty placeholder) `callLLM`. Never hit `/api/anthropic` directly.
chat/ (empty placeholder) - **New screen:** add a page under `src/pages/`, route it in `src/App.jsx`, and gate
progress/ (empty placeholder) it with `ProtectedRoute` (which also enforces curriculum enrollment).
- **New admin tool:** add a tab in `src/pages/Admin/index.jsx` and a panel under
`src/components/admin/`.
---
## Verification before shipping
```bash
npm test # Vitest unit tests (lib services)
npm run lint # ESLint
npm run build # production build to dist/
``` ```
Create `app/services/ingestion/` with: For UI/feature correctness, run the app against a local PocketBase
- package.json (dependencies from ingestion-spec.md) (`npm run dev` + `./pocketbase.exe serve`) and exercise the flow in the browser.
- tsconfig.json (strict mode)
- .env.example (all env vars from ingestion-spec.md)
- .gitignore
**1.2 — PocketBase collections**
PocketBase runs as a binary. Create a migration script at
`app/services/ingestion/migrations/001_initial_schema.ts` that uses the
PocketBase JS SDK to create all collections defined in data-model.md:
Collections to create:
- source_documents
- themes
- topics
- micro_learnings (schema only — no data yet)
- curriculum_versions (schema only)
- curriculum_weeks (schema only)
- employee_curriculum_state (schema only)
- session_completions (schema only)
- gamification_profiles (schema only)
- badges (schema only)
- employee_badges (schema only)
- milestone_cards (schema only)
Seed the badges collection with all badge definitions from data-model.md.
**1.3 — Qdrant collections**
Create `app/services/ingestion/migrations/002_qdrant_setup.ts` that
initialises both Qdrant collections:
- source_chunks (1536 dimensions, cosine distance)
- topic_summaries (1536 dimensions, cosine distance)
**1.4 — Ingestion service scaffold**
Build the Fastify server with two routes:
- POST /ingest
- GET /status/:jobId
Use the file structure from ingestion-spec.md exactly.
**1.5 — Stage 1: text extraction**
Implement extract.ts per ingestion-spec.md:
- TXT: direct UTF-8 read
- MD: direct UTF-8 read, preserve heading markers
- PDF: pdf-parse, page break markers
**1.6 — Stage 23: chunking + cleaning**
Implement chunk.ts and clean.ts per ingestion-spec.md:
- MD: heading-based splitting
- TXT: sliding window (800 chars, 150 overlap)
- PDF: page + paragraph splitting
- Cleaning: whitespace, artefacts, minimum length filter
**1.7 — Stage 4: structure extraction**
Implement structure.ts per ingestion-spec.md:
- Claude Sonnet 4 call with system + user prompt from spec
- Zod validation of DraftKB output
- Batch handling for documents > 60 chunks
- Retry logic on parse failure
- Error handling: failed job status + reason
**1.8 — Stage 5: PocketBase write**
Implement the PocketBase write logic:
- Create Theme records (status: draft)
- Create Topic records under each Theme (status: draft)
- Resolve relationships between Topics after all records created
**1.9 — Stage 6: embeddings + Qdrant write**
Implement embed.ts:
- OpenAI text-embedding-3-small, batches of 100
- Write to Qdrant source_chunks collection
- Write to Qdrant topic_summaries collection
- Update Topic.qdrant_chunk_ids in PocketBase
**1.10 — Job status tracking**
Wire all stages into the job queue (jobs/queue.ts):
- Status transitions: queued → extracting → chunking → structuring →
writing → embedding → done / failed
- Progress counters (chunksTotal, chunksEmbedded, themesFound, topicsFound)
- GET /status/:jobId returns current state
### Acceptance criteria
- [ ] POST /ingest with a small MD file completes without error
- [ ] GET /status/:jobId returns `done` after processing
- [ ] PocketBase contains draft Theme + Topic records with correct hierarchy
- [ ] Topic.body contains AI-drafted content (not empty)
- [ ] Topic relationships are resolved (related_topics populated where applicable)
- [ ] Qdrant source_chunks contains vectors with correct payload fields
- [ ] Qdrant topic_summaries contains vectors for each published topic
- [ ] Topic.qdrant_chunk_ids is populated
- [ ] POST /ingest with a PDF file completes without error
- [ ] POST /ingest with a TXT file completes without error
- [ ] A document > 60 chunks triggers batch processing without error
- [ ] A malformed PDF returns status `failed` with reason, not an uncaught exception
- [ ] All Zod validations pass — no `any` types in codebase
--- ---
## Phase 2 — Generation service ## Constraints (see CLAUDE.md / PROTECTED.md)
**Spec to read:** /docs/generation-spec.md (write this spec before starting) - Do not edit `stylesheet.css` or the deployment files (`Dockerfile`, `Caddyfile`,
`docker-compose.yml`, `infra/`, `.github/workflows/`) without a request.
### Steps - Do not re-enable PocketBase auto-cancellation.
- Do not re-add the podcast content type or the `reflection_prompt` micro-learning.
**2.1 — Generation service scaffold** - Ignore the abandoned `/app` Next.js scaffolding.
Fastify service at app/services/generation/
Routes: POST /generate, GET /status/:jobId
**2.2 — Generate all 10 types per topic**
One Claude Sonnet 4 call per type per topic.
Structured JSON output validated against Zod schemas from data-model.md.
Write to micro_learnings collection (status: generated).
**2.3 — Batch generation on theme approval**
When admin approves a Theme batch, queue generation for all Topics in that Theme.
All 10 types per Topic.
**2.4 — Admin publish flow**
Route to update micro_learning status from generated → published or rejected.
This is called by the admin app (built in phase 4).
### Acceptance criteria (to be detailed in generation-spec.md)
- [ ] All 10 micro learning types generated for a test topic
- [ ] All 10 JSON outputs validate against their Zod schemas
- [ ] Generated content written to PocketBase with status: generated
- [ ] Admin can publish or reject individual micro learnings
---
## Phase 3 — Curriculum service
**Spec to read:** /docs/curriculum-spec.md (write this spec before starting)
### Steps
**3.1 — Curriculum service scaffold**
Fastify service at app/services/curriculum/
**3.2 — Curriculum generator**
Claude Sonnet 4 reads full KB graph → produces 26-week schedule.
Written to curriculum_versions + curriculum_weeks.
**3.3 — Versioning logic**
- New version created on regeneration
- Completed weeks frozen (employee_curriculum_state.current_week used as boundary)
- Admin confirmation required before applying new version
**3.4 — Perpetual cycling**
On week 26 completion, cycle increments, new cycle starts on latest version.
Second cycle: varied sequence, surfaces unused micro learning types.
### Acceptance criteria (to be detailed in curriculum-spec.md)
- [ ] Curriculum generated from a populated KB
- [ ] 26 weeks produced, all Themes covered
- [ ] Prerequisites respected in ordering
- [ ] Regeneration does not alter completed weeks
- [ ] Admin confirmation flow works correctly
---
## Phase 4 — Frontend: admin app
**Spec to read:** /docs/frontend-spec.md (write this spec before starting)
### Steps
**4.1 — Next.js 14 scaffold**
Mobile-first, TypeScript strict, Tailwind CSS, PWA config.
Role-based routing: /admin/* and /app/* from single Next.js codebase.
**4.2 — Auth**
PocketBase auth integration. Admin role routes to /admin/*.
**4.3 — Document upload + ingestion status**
Upload UI → calls ingestion service → polls job status → shows progress.
**4.4 — Theme batch review**
Display draft Themes with their Topic list.
Approve batch / edit individual topics / reject batch.
Triggers generation service on approval.
**4.5 — Curriculum editor**
Display AI-generated curriculum (26 weeks).
Drag-to-reorder weeks. Edit Theme assignment per week.
Confirm regeneration with preview.
### Acceptance criteria (to be detailed in frontend-spec.md)
- [ ] Admin can upload a document and see ingestion progress
- [ ] Admin can approve a Theme batch
- [ ] Admin can edit a Topic before approval
- [ ] Admin can view and reorder the curriculum
- [ ] Admin can confirm a curriculum regeneration with preview
---
## Phase 5 — Frontend: employee app
**Spec to read:** /docs/frontend-spec.md (same file, employee section)
### Steps
**5.1 — Employee auth + onboarding**
PocketBase auth. Employee role routes to /app/*.
Set start date on first login → creates employee_curriculum_state record.
**5.2 — Weekly session flow**
Current week's Theme displayed.
Topics listed with available micro learning types per topic.
Employee selects type → content rendered → mark complete.
**5.3 — Knowledge library**
Browse all published Topics.
Filter by Theme, difficulty, key terms.
**5.4 — R42 chatbot**
Floating button, every screen.
Calls chat service → streams response.
Cites source topic in response.
**5.5 — Gamification profile**
GitHub-style heatmap (26-week view).
Badge display.
Streak + level + commit count.
Public leaderboard (multi-dimension).
Milestone cards in activity feed.
### Acceptance criteria (to be detailed in frontend-spec.md)
- [ ] Employee sees correct week based on start date
- [ ] Employee can complete a topic with a chosen micro learning type
- [ ] Completion is recorded and XP awarded
- [ ] Knowledge library shows all published topics with filters
- [ ] R42 responds with grounded answer and source citation
- [ ] R42 is accessible from every screen
- [ ] Heatmap renders correctly on mobile (375px)
- [ ] Leaderboard shows all employees with multi-dimension data
---
## Phase 6 — Chat service (R42)
**Spec to read:** /docs/r42-spec.md (write this spec before starting)
### Steps
**6.1 — Chat service scaffold**
Fastify service at app/services/chat/
**6.2 — Query → embed → retrieve**
Employee query embedded → Qdrant nearest-neighbour on both collections.
Boost chunks from employee's current Theme.
**6.3 — Response generation**
Top-K chunks injected into Haiku 4.5 prompt.
Response streamed to frontend.
Source Topic titles included in response.
**6.4 — Out-of-scope handling**
If retrieval confidence is below threshold, R42 responds:
"I can only answer questions based on the internal knowledge base.
This topic doesn't appear to be covered."
### Acceptance criteria (to be detailed in r42-spec.md)
- [ ] R42 answers a question about a published topic correctly
- [ ] R42 cites the source topic in its response
- [ ] R42 refuses to answer out-of-scope questions explicitly
- [ ] Response streams to frontend (not batch)
- [ ] Response latency < 3 seconds for typical queries
---
## Phase 7 — Progress service
**Spec to read:** /docs/gamification-spec.md (write this spec before starting)
### Steps
**7.1 — Progress service scaffold**
Fastify service at app/services/progress/
**7.2 — Completion recording**
Write session_completions record on topic completion.
Calculate XP (commits) per type.
**7.3 — Gamification updates**
Update gamification_profiles: commits, level, streak, types_used.
Evaluate badge conditions → write employee_badges on award.
**7.4 — Milestone cards**
Generate milestone_cards record at weeks 13 and 26.
**7.5 — Leaderboard query**
Endpoint returning all gamification_profiles for leaderboard rendering.
### Acceptance criteria (to be detailed in gamification-spec.md)
- [ ] Completion writes to session_completions
- [ ] Commits calculated and added to gamification_profile
- [ ] Level updates correctly at commit thresholds
- [ ] Streak increments on weekly completion, resets on skip
- [ ] Badge awarded when condition is met
- [ ] Milestone card created at weeks 13 and 26
- [ ] Leaderboard endpoint returns all employees with correct data
---
## Phase 8 — Integration + hardening
No new spec required.
### Steps
**8.1 — Service wiring**
Verify all services communicate through PocketBase correctly.
No direct service-to-service calls — all state through PocketBase.
**8.2 — Error handling audit**
Review all services for unhandled promise rejections, missing error states,
and uncaught exceptions. Every external call (AI API, PocketBase, Qdrant,
OpenAI) wrapped in try/catch with meaningful error logging.
**8.3 — Mobile QA**
Test all employee app flows at 375px width.
R42 floating button must not obscure content.
Heatmap must render without horizontal scroll.
**8.4 — Environment variable audit**
Verify no hardcoded values. All .env.example files complete.
**8.5 — Dockerfile update**
Update COPY path from legacy app root to /app.
This is the one manual change that connects the rebuild to the existing pipeline.
### Acceptance criteria
- [ ] Full flow works end-to-end: upload doc → approve → curriculum → employee completes session → R42 answers question → gamification updates
- [ ] No uncaught exceptions in any service under normal operating conditions
- [ ] All screens render correctly on 375px mobile
- [ ] Dockerfile builds successfully pointing at /app
- [ ] Existing pipeline deploys the rebuilt app without modification
---
## Spec files still to be written
Before starting each phase, write the corresponding spec file.
Use ingestion-spec.md as the template for structure and detail level.
| Phase | Spec file needed |
|---|---|
| 2 | /docs/generation-spec.md |
| 3 | /docs/curriculum-spec.md |
| 45 | /docs/frontend-spec.md |
| 6 | /docs/r42-spec.md |
| 7 | /docs/gamification-spec.md |
When you reach a phase without a spec: stop, draft the spec, then proceed.
Do not implement without a spec.

View File

@@ -1,483 +1,72 @@
# Ingestion service spec # Ingestion spec: source documents → knowledge graph
## Responsibility Turns admin-uploaded text into `topics` and `relations` using Claude. Runs
entirely client-side; there is no ingestion service.
Accepts uploaded source documents (PDF, MD, TXT), extracts clean text, chunks it, - **UI:** `src/components/admin/UploadZone.jsx` (Admin → Sources tab)
generates embeddings, and produces a structured draft KB (Themes + Topics + - **Pipeline:** `src/lib/extractionPipeline.js`
relationships) ready for admin review. - **Tool/schema:** `emit_knowledge_graph` (`src/lib/llmTools.js`) validated by
`extractionResultSchema` (`src/lib/llmSchemas.js`)
This service runs entirely server-side. The admin app calls it via REST. All AI
calls go through the Anthropic API. No ingestion logic lives in the frontend.
--- ---
## Service location ## Upload
``` - Accepted formats: **`.txt` and `.md`**, max **5 MB** per file.
app/services/ingestion/ - Drag-and-drop or click-to-browse. Unsupported files are skipped with a toast.
├── index.ts entry point, Fastify server - The queue tracks each file: `pending → processing → done / failed / cancelled`.
├── routes/ - Progress is polled every ~2s from the `sources.progress` field
│ └── documents.ts POST /ingest, GET /status/:jobId (`{ current, total, message }`), shown as "Chunk N/total".
├── pipeline/ - **Orphan detection:** sources stuck in `processing` for >5 minutes (e.g. a closed
│ ├── extract.ts format detection + text extraction tab) can be marked failed or deleted.
│ ├── chunk.ts chunking strategies per format
│ ├── clean.ts chunk cleaning
│ ├── structure.ts Claude call → Theme/Topic extraction
│ └── embed.ts embedding generation + Qdrant write
├── jobs/
│ └── queue.ts async job queue (in-memory, BullMQ later if needed)
├── lib/
│ ├── pocketbase.ts PocketBase client
│ ├── qdrant.ts Qdrant client
│ ├── anthropic.ts Anthropic client
│ └── openai.ts OpenAI embeddings client
└── types.ts shared TypeScript types
```
--- ---
## API surface ## Pipeline: `processSourceText(textContent, sourceName, { signal })`
### POST /ingest 1. **Create source record** with `status='processing'`.
2. **Chunk** the text (`chunkText`): target ~8000 chars per chunk with ~800 chars
overlap, splitting on sentence/paragraph boundaries (hard-splitting oversized
sentences). Overlap preserves cross-boundary context.
3. **Known-topics hint** (`buildKnownIdsHint`): up to the 200 most recent existing
topics are listed so the model reuses existing ids instead of duplicating them.
4. **Per-chunk extraction** via `callLLM`:
- tier `standard`, `maxTokens: 8192`, `timeoutMs: 180_000`
- forced `toolChoice` on `emit_knowledge_graph`
- rate-limited (~20 req/min, burst 2) to protect quota across many chunks
- system prompt instructs: ≤15 topics per chunk; topic `type`
{`concept`, `role`, `process`}; `learning_relevance`
{`core`, `standard`, `peripheral`, `exclude`}; relation `type`
{`related_to`, `depends_on`, `part_of`, `executed_by`}
5. **Update progress** before each chunk.
6. **Merge** (`mergeKnowledgeGraph`): topics keyed by `id` (new data updates
existing, but `learning_relevance` is preserved when `relevance_locked` is true);
relations de-duplicated on `(source, target, type)`. Persisted via
`db.saveTopics` / `db.saveRelations`.
7. **Finalize:** `status='completed'`, or `failed` (error) / `cancelled` (abort).
Triggered by admin app on document upload. Aborting via the `signal` stops the run and marks the source `cancelled`.
Request:
```json
{
"documentId": "string",
"filename": "string",
"format": "pdf" | "md" | "txt",
"filePath": "string"
}
```
Response (202 Accepted):
```json
{
"jobId": "string",
"status": "queued"
}
```
Processing is async. The admin app polls job status.
--- ---
### GET /status/:jobId ## Output shape (`emit_knowledge_graph`)
Returns current job progress.
Response:
```json
{
"jobId": "string",
"status": "queued" | "extracting" | "chunking" | "structuring" | "embedding" | "done" | "failed",
"progress": {
"chunksTotal": 42,
"chunksEmbedded": 18,
"themesFound": 3,
"topicsFound": 14
},
"error": "string | null"
}
```
---
## Pipeline stages
### Stage 1 — Text extraction
Input: file path + format
Output: raw text string
```
format === 'txt'
→ read file directly as UTF-8
format === 'md'
→ read file directly as UTF-8
→ preserve heading markers (# ## ###) — used in chunking
format === 'pdf'
→ pdfplumber: extract text page by page
→ concatenate with page break markers: \n\n---PAGE---\n\n
→ strip known PDF artefacts: headers/footers repeating on every page,
page numbers, watermarks
```
Failure handling:
- PDF extraction returns empty string → mark job `failed`, reason: `pdf_extraction_empty`
- File not found → mark job `failed`, reason: `file_not_found`
---
### Stage 2 — Chunking
Input: raw text + format
Output: Chunk[]
Chunking strategy differs per format.
**MD chunking — heading-based (preferred)**
```
Split on heading markers: #, ##, ###
Each heading + its following content = one chunk
Minimum chunk size: 100 characters
→ if heading section is < 100 chars, merge with next sibling
Maximum chunk size: 1500 characters
→ if section exceeds limit, split on paragraph breaks within section
Metadata preserved per chunk:
heading_level: 1 | 2 | 3
heading_text: string
parent_heading: string | null
```
MD chunking produces the highest quality structural signal for Theme/Topic extraction.
Admins should be advised to provide source material as MD where possible.
**TXT chunking — sliding window**
```
Window size: 800 characters
Overlap: 150 characters
Split on: paragraph breaks (\n\n) first, then sentence boundaries, then hard cut
Metadata per chunk:
chunk_index: number
approximate_position: 'start' | 'middle' | 'end'
```
**PDF chunking — page + paragraph**
```
Split on ---PAGE--- markers from extraction stage
Within each page: split on paragraph breaks (\n\n)
Minimum chunk size: 100 characters
→ merge sub-threshold paragraphs with adjacent chunk
Maximum chunk size: 1200 characters
→ hard split at sentence boundary
Metadata per chunk:
page_number: number
chunk_index_on_page: number
```
**Chunk type:**
```typescript
type Chunk = {
id: string // UUID generated at chunking
documentId: string
text: string
format: 'pdf' | 'md' | 'txt'
index: number // global position in document
metadata: {
// MD-specific
headingLevel?: number
headingText?: string
parentHeading?: string
// TXT-specific
approximatePosition?: 'start' | 'middle' | 'end'
// PDF-specific
pageNumber?: number
chunkIndexOnPage?: number
}
}
```
---
### Stage 3 — Chunk cleaning
Input: Chunk[]
Output: Chunk[] (cleaned)
Applied to all formats:
```
- trim leading/trailing whitespace
- collapse 3+ consecutive newlines to 2
- remove null bytes and non-printable characters
- remove chunks where text.length < 80 after cleaning
→ these are likely artefacts (page numbers, standalone headers)
- normalise unicode: NFC normalisation
- do not strip punctuation or alter sentence structure
```
---
### Stage 4 — Structure extraction (AI)
Input: Chunk[]
Output: DraftKB
This is the core AI call. Claude Sonnet 4 reads all chunks and returns a structured
KB draft as JSON.
**Prompt strategy:**
System prompt:
```
You are a knowledge architect. Your task is to analyse a set of text chunks from
a source document and extract a structured knowledge base.
Output ONLY valid JSON matching the schema provided. No preamble, no explanation,
no markdown fences.
Rules:
- Group related content into Themes. A Theme is a broad subject area.
- Under each Theme, identify discrete Topics. A Topic covers one specific concept.
- Identify relationships between Topics: related, prerequisite, or contrast.
- related: Topics that complement each other
- prerequisite: Topic A must be understood before Topic B
- contrast: Topics that represent opposing approaches or concepts
- For each Topic, extract key terms suitable for a glossary.
- Assign a complexity weight (15) to each Topic.
1 = introductory, 5 = advanced
- Draft a body for each Topic (24 paragraphs) based on the source chunks.
- Draft a description for each Theme (12 sentences).
- Every Topic must reference the chunk IDs that contributed to it.
```
User prompt:
```
Source document: {filename}
Format: {format}
Chunks:
{chunks mapped as: [CHUNK-{id}]\n{text}\n}
Extract the knowledge base structure from these chunks.
```
**Output schema:**
```typescript
type DraftKB = {
themes: DraftTheme[]
}
type DraftTheme = {
title: string
description: string
topics: DraftTopic[]
}
type DraftTopic = {
title: string
body: string
difficulty: 'introductory' | 'intermediate' | 'advanced'
complexityWeight: number // 15
keyTerms: string[]
sourceChunkIds: string[] // references Chunk.id values
relationships: {
related: string[] // topic titles (resolved to IDs after write)
prerequisites: string[]
contrasts: string[]
}
}
```
**AI call configuration:**
```typescript
{
model: 'claude-sonnet-4-20250514',
max_tokens: 8000,
temperature: 0 // deterministic output for structured extraction
}
```
**Chunking strategy for large documents:**
If total chunk count exceeds 60 chunks, split into batches of 40 with 5-chunk
overlap. Run one Claude call per batch. Merge resulting DraftKB objects:
- Themes with identical titles → merge Topics
- Duplicate Topic titles within a Theme → keep longer body, merge sourceChunkIds
- Relationships are resolved after full merge
**Error handling:**
- JSON parse failure → retry once with stricter prompt ("ensure valid JSON only")
- Second failure → mark job `failed`, reason: `structure_extraction_failed`, log raw response
- Empty themes array → mark job `failed`, reason: `no_structure_found`
---
### Stage 5 — Write to PocketBase
Input: DraftKB
Output: written Theme + Topic records with status `draft`
```
For each DraftTheme:
create themes record {
title, description,
status: 'draft',
source_documents: [documentId]
}
For each DraftTopic under the theme:
create topics record {
theme: themeId,
title, body, difficulty, complexity_weight, key_terms,
status: 'draft',
qdrant_chunk_ids: [] // populated in stage 6
}
After all topics created:
resolve relationship titles → topic IDs
update topics.related_topics, prerequisite_topics, contrast_topics
If a relationship title cannot be resolved to an existing topic:
skip silently (cross-document relationships resolved in a later pass)
```
---
### Stage 6 — Embedding generation + Qdrant write
Input: Chunk[], written Topic records
Output: vectors in Qdrant, qdrant_chunk_ids updated on Topic records
**Source chunk embeddings:**
```
For each Chunk (post-cleaning):
embed Chunk.text → text-embedding-3-small (1536 dimensions)
write to Qdrant collection: source_chunks {
id: Chunk.id,
vector: float[],
payload: {
source_document_id: documentId,
chunk_index: Chunk.index,
text: Chunk.text,
theme_id: resolved themeId | null,
topic_id: resolved topicId | null,
format: Chunk.format
}
}
```
**Topic summary embeddings:**
```
For each published Topic:
embed Topic.body → text-embedding-3-small
write to Qdrant collection: topic_summaries {
id: UUID,
vector: float[],
payload: {
topic_id: Topic.id,
theme_id: Topic.theme,
title: Topic.title,
text: Topic.body
}
}
Update Topic.qdrant_chunk_ids with all Chunk.ids that reference this topic
```
**Batching:**
OpenAI embeddings API: batch in groups of 100 texts per request to stay within
rate limits and reduce latency.
---
## Job lifecycle
```
POST /ingest received
Job created → status: queued
Stage 1: extracting
Stage 23: chunking
Stage 4: structuring
Stage 5: writing to PocketBase
Stage 6: embedding
status: done
Admin notification: "Document processed. N themes, N topics ready for review."
Curriculum regeneration queued (status: pending_admin_confirm)
```
On any stage failure:
```
status: failed
error: { stage, reason, detail }
Source document status → 'failed' in PocketBase
Admin notification: "Ingestion failed: {reason}"
```
---
## Environment variables required
```
ANTHROPIC_API_KEY=
OPENAI_API_KEY=
POCKETBASE_URL=
POCKETBASE_ADMIN_EMAIL=
POCKETBASE_ADMIN_PASSWORD=
QDRANT_URL=
QDRANT_API_KEY= # empty string if running locally without auth
INGESTION_PORT=3001
```
---
## Dependencies
```json ```json
{ {
"dependencies": { "topics": [ { "id", "label", "type", "description", "learning_relevance" } ],
"fastify": "^4", "relations":[ { "source", "target", "type" } ]
"@anthropic-ai/sdk": "^0.24",
"openai": "^4",
"@qdrant/js-client-rest": "^1.9",
"pocketbase": "^0.21",
"pdfplumber": "NOT JS — see note below",
"pdf-parse": "^1.1",
"uuid": "^9",
"zod": "^3"
}
} }
``` ```
**PDF extraction note:** `theme`, `complexity_weight`, and `difficulty` are **not** set here — they are
`pdfplumber` is a Python library. Two options: added later by the curriculum enrichment step (see `docs/curriculum-spec.md`).
1. Use `pdf-parse` (Node.js) — simpler, covers 90% of cases
2. Run `pdfplumber` as a Python sidecar process via child_process — higher quality
for complex PDFs with tables and columns
Default to `pdf-parse` initially. Add pdfplumber sidecar only if extraction
quality is insufficient for actual source documents.
--- ---
## TypeScript strict mode requirements ## Gotchas
- No `any` types - If extraction logs a truncation (`LLMTruncatedError`, `stop_reason: max_tokens`),
- All Claude response parsing through Zod schema validation tighten the per-chunk topic cap before raising `max_tokens`.
- All PocketBase writes typed against collection schemas from `data-model.md` - A source already `completed` is not re-processed; delete it to force re-analysis.
- Qdrant payloads typed explicitly — no untyped objects - There are no embeddings produced here — R42 retrieval is computed at query time
with TF-IDF over `topics`.
---
## What this service does NOT do
- Does not generate micro learnings → generation service
- Does not build or update the curriculum → curriculum service
- Does not handle admin approval → admin app + PocketBase directly
- Does not serve R42 queries → chat service
- Does not handle auth → PocketBase + admin app
---
## Testing checkpoints
Before handing to Claude Code for implementation, verify manually:
1. Upload a short MD file (< 10 headings) → inspect chunk output → confirm heading structure preserved
2. Upload a simple PDF (< 5 pages) → inspect chunk output → confirm no artefacts
3. Run structure extraction on known chunks → validate JSON parses against Zod schema
4. Confirm PocketBase draft records created with correct theme → topic hierarchy
5. Confirm Qdrant source_chunks collection populated with correct payload fields
6. Confirm topic.qdrant_chunk_ids updated after embedding stage

View File

@@ -1,336 +1,68 @@
# R42 chat service spec # R42 spec: the in-app chatbot
## Responsibility R42 is a knowledge-base-grounded assistant available on every screen. It runs
client-side and is grounded by local TF-IDF retrieval — **no vector database**.
Handles all R42 chatbot interactions. Receives employee queries, retrieves - **UI:** `src/components/chat/``ChatLauncher.jsx`, `ChatWindow.jsx`, `useChat.js`
relevant KB chunks from Qdrant, generates grounded responses using Claude - **Prompt/tool:** `src/components/chat/prompts.js`
Haiku 4.5, and streams the result to the frontend. Stateless — no chat - **Retrieval/validation:** `src/components/chat/rag.js` + `src/lib/retrieval.js`
history is persisted. - **KB writes:** `src/lib/kbStore.js`
--- ---
## Service location ## Conversation
``` - The brand mark (`src/components/ui/Mark.jsx`) renders R42 in idle / typing / error states.
app/services/chat/ - Messages persist per user in `localStorage` under `chat:thread:{userId}`, capped at
├── src/ **50** messages. Only roughly the last **12** turns are sent to the API; older
│ ├── index.ts entry point, Fastify server context is truncated with a notice.
│ ├── routes/ - A greeting message seeds an empty thread.
│ │ └── chat.ts POST /chat (streaming) - Each turn calls `callLLM` (fast/standard Claude tier — low latency matters for chat).
│ ├── retrieval/
│ │ ├── embed.ts query → embedding
│ │ ├── search.ts Qdrant nearest-neighbour search
│ │ └── merge.ts merge + rank results from both collections
│ ├── prompt/
│ │ └── build.ts assemble system + user prompt with context
│ └── lib/
│ ├── qdrant.ts
│ ├── pocketbase.ts
│ ├── anthropic.ts
│ └── openai.ts
├── package.json
├── tsconfig.json
└── .env.example
```
--- ---
## API surface ## Grounding (RAG via TF-IDF)
### POST /chat `buildKbContext` in `rag.js`:
1. Build / reuse the TF-IDF index over `topics` (`src/lib/retrieval.js`).
2. Retrieve the top **10** topics for the user's message.
3. Always include topics whose `id` or `label` appears verbatim in the message.
4. Include relations only when **both** endpoints are in the retrieved set.
5. For explicitly mentioned topics, inject up to ~1200 chars of their generated
content.
6. Append a short KB hash so the cached context busts when topics change.
Single route. Streams response back to client using server-sent events (SSE). The system prompt (`prompts.js`) is assembled as cacheable blocks: a stable
preamble (role, tasks, style, "answer only from the KB"), the KB context block, and
Request: a per-turn tail with the user's name and admin/non-admin flag.
```json
{
"query": "string",
"userId": "string"
}
```
Response: SSE stream
```
Content-Type: text/event-stream
data: {"type": "chunk", "text": "Holacratic roles "}
data: {"type": "chunk", "text": "are defined as..."}
data: {"type": "citations", "topics": [{"id": "abc", "title": "Holacratic roles"}]}
data: {"type": "done"}
```
Error response (non-streaming, returned before stream starts):
```json
{
"error": "query_too_short" | "user_not_found" | "retrieval_failed",
"message": "string"
}
```
--- ---
## Retrieval pipeline ## Proposing knowledge-graph edits
### Step 1 — Embed query R42 may call `propose_graph_delta` with:
- `reason` — one sentence
- `topics`**max 3**, each `{ id (kebab-case), label, type (concept|role|process), description }`
- `relations`**max 5**, each `{ source, target, type (related_to|depends_on|part_of|executed_by) }`
Embed the employee query using OpenAI text-embedding-3-small (1536 dimensions). `validateDelta` (`rag.js`) dedupes by topic id and case-folded label, rejects
Same model used during ingestion — vectors are comparable. relations with missing endpoints or self-references, and enforces the 3/5 caps.
Invalid deltas are dropped.
```typescript A confirmation chip appears inline:
const queryVector = await embedText(query) // float[1536] - **Admin → Yes:** `kbStore.applyDelta` writes topics/relations to PocketBase immediately.
``` - **Non-admin → Yes:** `kbStore.appendSuggestion` queues a `pending` entry in
`kb:suggestions` (localStorage) for admin review.
Admins review the queue in `src/components/admin/SuggestionsQueue.jsx` (approve
re-runs `applyDelta`; reject marks it rejected). `kbStore` dispatches
`respellion:kb-updated` after writes so the D3 graph and queue refresh.
--- ---
### Step 2 — Qdrant search ## Quiz integrity
Search both collections in parallel: `src/pages/Testen.jsx` sets `quiz:active:{userId}=true` on quiz start and clears it
on every non-quiz phase and on unmount, dispatching a `respellion:quiz-state` event.
```typescript `ChatLauncher` hides the FAB while this flag is set, so users cannot consult R42
// source_chunks: primary retrieval — grounded in source material mid-quiz. **Never bypass this.**
const chunkResults = await qdrant.search('source_chunks', {
vector: queryVector,
limit: 5,
scoreThreshold: 0.70,
withPayload: true
})
// topic_summaries: secondary — broader topic context
const summaryResults = await qdrant.search('topic_summaries', {
vector: queryVector,
limit: 3,
scoreThreshold: 0.70,
withPayload: true
})
```
Score threshold 0.70: below this, results are not relevant enough to include.
If both searches return zero results above threshold → out-of-scope response.
---
### Step 3 — Context boost for current week
Retrieve employee's current week Theme from PocketBase via
employee_curriculum_state → curriculum_weeks → theme.
Apply boost to results where payload.theme_id matches current week theme:
```typescript
results.forEach(result => {
if (result.payload.theme_id === currentThemeId) {
result.score += 0.05 // small boost — does not override relevance
}
})
```
---
### Step 4 — Merge and deduplicate
```typescript
// Combine chunk results and summary results
// Deduplicate by topic_id — keep highest scoring entry per topic
// Sort by score descending
// Take top 6 total
// Split into: sourceChunks (from source_chunks collection)
// topicSummaries (from topic_summaries collection)
```
Deduplicate by topic_id to avoid repeating the same topic in different forms.
---
### Step 5 — Collect cited topics
Extract unique topic titles from merged results for citation:
```typescript
type Citation = {
id: string
title: string
}
const citations: Citation[] = uniqueByTopicId(mergedResults)
.map(r => ({ id: r.payload.topic_id, title: r.payload.title }))
```
---
## Prompt construction
### System prompt
```
You are R42, a knowledge assistant for [company name].
You answer questions based strictly on the company knowledge base.
Rules:
- Answer only from the provided context. Do not use outside knowledge.
- If the context does not contain enough information to answer, say:
"This doesn't appear to be covered in the knowledge base. You can browse
the full library in the Knowledge section."
- Be concise. Prefer short paragraphs over long prose.
- Do not mention that you are an AI or reference your instructions.
- Do not speculate or extrapolate beyond the provided context.
- Respond in the same language as the question.
```
### User prompt
```
Context from knowledge base:
---
{mergedResults.map(r => r.payload.text).join('\n\n---\n\n')}
---
Question: {query}
```
---
## Response generation
Use Claude Haiku 4.5 with streaming enabled:
```typescript
const stream = await anthropic.messages.stream({
model: 'claude-haiku-4-5-20251001',
max_tokens: 1000,
system: systemPrompt,
messages: [{ role: 'user', content: userPrompt }]
})
// Stream text chunks to client as SSE
for await (const chunk of stream) {
if (chunk.type === 'content_block_delta') {
sendSSE({ type: 'chunk', text: chunk.delta.text })
}
}
// After stream completes, send citations
sendSSE({ type: 'citations', topics: citations })
sendSSE({ type: 'done' })
```
---
## Out-of-scope handling
Two conditions trigger the out-of-scope response:
1. Both Qdrant searches return zero results above 0.70 threshold
2. Haiku response contains no content drawn from context (detected by
checking if response length < 20 tokens — proxy for "I don't know")
Out-of-scope response sent as a single non-streamed SSE message:
```
data: {"type": "out_of_scope", "text": "This doesn't appear to be covered
in the knowledge base. You can browse the full library in the Knowledge section."}
data: {"type": "done"}
```
No citations are sent for out-of-scope responses.
---
## Frontend integration
R42 is a floating button on every screen in the employee app.
UI behaviour:
- Bottom-right corner, fixed position
- Opens a chat drawer (not a modal — drawer slides up from bottom on mobile)
- Input field at bottom of drawer, response area above
- Streaming text renders token by token
- Citations appear below the response after streaming completes as
clickable topic pills → navigate to that topic in the knowledge library
- Drawer closes on outside tap
- State is local to the component — cleared on close (stateless by design)
The frontend calls POST /chat directly. No auth token needed on the chat
service — it receives userId in the request body and trusts it. The admin
app does not expose R42.
---
## Stateless design
R42 has no memory between conversations. Each POST /chat is independent.
Rationale:
- Avoids privacy complexity around chat history storage
- Removes need for session management
- Keeps the service simple and fast
- Employees asking follow-up questions reprovide context naturally
If multi-turn conversation is needed in a future iteration, maintain
conversation history in the frontend component state and pass the last
N messages in the request body. The service does not need to change.
---
## Environment variables
```
ANTHROPIC_API_KEY=
OPENAI_API_KEY=
POCKETBASE_URL=
POCKETBASE_ADMIN_EMAIL=
POCKETBASE_ADMIN_PASSWORD=
QDRANT_URL=
QDRANT_API_KEY=
CHAT_PORT=3004
```
---
## Dependencies
```json
{
"dependencies": {
"fastify": "^4",
"@fastify/sse": "^2",
"@anthropic-ai/sdk": "^0.24",
"openai": "^4",
"@qdrant/js-client-rest": "^1.9",
"pocketbase": "^0.21",
"zod": "^3"
}
}
```
---
## TypeScript strict mode requirements
- No `any` types
- Qdrant search results typed explicitly including payload fields
- SSE event types defined as a discriminated union
- Citation type explicit — not inferred from payload
---
## What this service does NOT do
- Does not persist chat history
- Does not generate or serve micro learning content
- Does not handle admin queries — admin app has no R42 access
- Does not handle auth — trusts userId from request body
---
## Testing checkpoints
1. POST /chat with a query matching a published topic → confirm relevant
chunks retrieved (score > 0.70) and response references topic content
2. POST /chat with an out-of-scope query → confirm out-of-scope response
returned, no citations sent
3. Confirm citations array contains correct topic titles matching retrieved chunks
4. Confirm SSE stream delivers chunks progressively (not batched)
5. Confirm current-week boost: same query returns higher-ranked result for
current week theme topic vs equally relevant topic from different theme
6. POST /chat with userId whose current week has no matching topic →
confirm boost does not break retrieval, general results returned

View File

@@ -0,0 +1,150 @@
---
title: Day to Day
category: Day to day
source_files:
- docs/Day to day/overtime-flexible-hours.md
- docs/Day to day/office-presence.md
- docs/Day to day/time-registration.md
- docs/Day to day/office-etiquette.md
- docs/Day to day/travel-time-policy.md
- docs/Day to day/printer.md
summary: >
Practical daily-work policies: overtime and flexible hours with Time-for-Time
(TfT), office presence registration, Exact time registration, office etiquette,
travel-time compensation, and printing via PaperCut Hive.
tags:
- overtime
- flexible-hours
- time-for-time
- tft
- office-presence
- time-registration
- exact
- etiquette
- travel-time
- printing
related_topics:
- holiday-and-leave.md # leave registration in Exact, sick leave reporting
- pension-scheme-and-benefits.md # travel allowance, benefits overview
- spending-and-contracting.md # travel km allowance, expense submission
- how-we-work-together.md # core values behind flexibility/etiquette
key_facts:
tft_compensation_weekday: 100% (1-for-1)
tft_compensation_saturday_sunday: 175%
tft_compensation_public_holiday: 300%
tft_savings_maximum_hours: 24
travel_time_unpaid_threshold_hours_one_way: 1.5
travel_time_compensation_above_threshold: "1:1"
time_registration_tool: Exact Online
presence_tool: Outlook Calendar
printing_service: PaperCut Hive
ai_context: >
This is the canonical home for Time-for-Time (TfT) accrual and the overtime
compensation rates. The leave section (holiday-and-leave.md) also registers TfT
in Exact but defers to this file for the concept and weekend/holiday rates.
"Time-for-Time" = "Tijd-voor-tijd" in Exact. Travel TIME compensation (the
1.5-hour threshold here) is distinct from the travel KILOMETER allowance
(EUR 0.23/km) which lives in spending-and-contracting.md and the benefits
overview.
---
# Day to Day
This section covers the practical rhythm of working at Respellion: overtime and flexible hours, presence registration, time registration in Exact, office etiquette, travel-time compensation, and printing.
## Overtime and Flexible Hours
At Respellion, we strive for a sustainable and healthy work-life balance. This aligns directly with our core value of Self-Discipline. We trust that as a professional you take personal responsibility for maintaining healthy working hours. These guidelines provide a framework to facilitate flexibility, protect you, and simultaneously offer the autonomy that fits our culture. In exceptional cases, there is always the opportunity to discuss possibilities.
### 1. Framework for flexibility and saving hours (Employee's Choice)
We understand that you may need flexibility. Therefore, it is possible to manage your contract hours flexibly within a month, or to work extra hours to save for time off at a later moment. There is no obligation to use these saved hours within a specific period.
- **Time-for-time as the basis:** Hours you work beyond the daily norm are automatically processed as saved leave hours. These hours are compensated on a 1-for-1 basis (100%) on weekdays.
- **Savings Maximum:** To maintain balance, there is a maximum of 24 hours above the normal non-statutory balance.
- **Personal Responsibility:** It is your responsibility to monitor your saved-hours balance and to take your time off promptly to avoid exceeding the 24-hour limit. This is a direct example of the self-discipline we value highly at Respellion.
#### Administration — Time-for-Time (TfT)
If you work more hours than your contract stipulates in a month, these extra hours will be added to your TfT balance at the beginning of the following month.
1. **Accrual:** At the beginning of each month, the overtime hours from the previous month are manually added to your TfT balance. You can see this as a separate balance in your leave administration.
2. **Taking Leave:**
- Go to **Leave Registration** in Exact.
- Click on **"Create leave registration"**.
- Select your own name.
- Under **Leave Type**, choose **"Tijd-voor-tijd" (Time-for-time)**.
- Enter the number of hours you wish to take.
- **Please note:** ensure you do not create a negative TfT balance.
### 2. Overtime by Assignment (Client Necessity)
While our policy is aimed at preventing overtime, it may occur that a client requests extra effort. In such cases, the following framework applies:
- **Consultation is crucial:** Overtime at a client's request always takes place in consultation with, and with approval of, the responsible role. We will take your personal situation into account as much as possible.
- **Compensation:** Overtime on weekdays is standardly compensated with time-for-time (100%). For necessary work on Saturdays, Sundays, and public holidays, significantly higher compensation rates apply: **175% for weekend days** and **300% for public holidays**. These hours are also primarily compensated in time.
### 3. Payout of Saved Hours
We offer the option to have saved hours paid out. It is your own responsibility to indicate in a timely manner if you wish to have saved hours paid out. This aligns with the entrepreneurial attitude we encourage in all our colleagues.
## Office Presence
To keep track of who is in the office and when, you can set your planned presence in Outlook Calendar. For each workday, indicate whether you expect to be working from home (Remote) or at the office (Office).
Of course, not every week is the same. In that case, select your average presence and then adjust this in the weekly overview for the days that deviate from the schedule.
[See further instructions in the manual](https://respellion.sharepoint.com/sites/Company/Shared%20Documents/Forms/AllItems.aspx?id=%2Fsites%2FCompany%2FShared%20Documents%2FHandleiding%20%2D%20Locatie%20instellen%20in%20Outlook%2Epdf&parent=%2Fsites%2FCompany%2FShared%20Documents).
## Exact Time Registration
We use Exact Online as financial administration and for time registration in projects, internal activities, and absence. The following instructions cover the steps to register your hours on the various types.
You need to register all contract hours, so if you work 36 hours, you register only 36 hours.
- Mind that you register your hours according to the appropriate project type, either Time and Material (TM) or Fixed Price Projects (FPP).
- Regarding registering your absence: please **do not** register hours of absence in your time registration for your project(s). You can simply leave these empty in your daily or weekly registration. (All absence is registered via the leave registration module — see [Holiday and Leave → Leave administration in Exact](holiday-and-leave.md).)
- Time spent on internal activities needs to be specified on the correct "Uursoort" in project **INTBD001 — Business Development**.
- Time spent on our werk- en rol-overleggen has a specific "uursoort".
### Sickness registration
When you are ill, you do not need to register any hours in Exact yourself. Do report your illness to the People Officer role, according to the procedure in our absence protocol. The People Officer will then ensure your sick leave is registered correctly in Exact. (Full sick-leave procedure: [Holiday and Leave → Sick leave](holiday-and-leave.md).)
## Office Etiquette
At Respellion, we strive for a productive and respectful work environment for everyone, including during remote meetings held from the office. To minimize disruptions and ensure optimal concentration, the following guidelines apply:
- **Larger meetings and demonstrations:** For meetings with multiple participants or for demonstrations, it is essential to reserve a separate meeting room. This ensures that others in the open-plan office are not disturbed and that meeting participants can concentrate optimally.
- **Individual calls and short discussions:** Brief, individual calls or quick discussions via a remote connection are acceptable in the open office space. However, always be mindful of your volume and try to cause as little inconvenience as possible to your colleagues. Keep the duration of such calls in the open space limited.
These guidelines contribute to our collective well-being and efficient way of working, in line with our core values such as trust and self-discipline, which are also reflected in our ability to be considerate of one another.
## Travel Time Policy
At Respellion, we recognize that business travel is sometimes necessary to serve our clients effectively. This policy outlines how travel **time** is compensated, ensuring a fair balance between business needs and personal time.
- Employees may claim travel time that exceeds 1.5 hours (one-way).
- For any single journey, the first 1.5 hours of travel time (one-way) is considered personal time and will not be compensated.
- Any travel time beyond 1.5 hours per single journey can be registered and will be compensated at a 1:1 ratio.
- This policy applies to occasional travel requirements. For structural travel patterns that consistently exceed this threshold, individual arrangements may be discussed.
- Employees should register travel time using the appropriate hour type in the time registration system.
> The travel **kilometer** allowance (EUR 0.23/km) is a separate matter — see [Spending and Contracting → Travel](spending-and-contracting.md) and the [benefits overview](pension-scheme-and-benefits.md).
## Printing — PaperCut Hive
We can print documents with the PaperCut Hive service, which requires you to create an account. This system will handle all your printing. PaperCut will guide you through the setup process, but here's a quick overview:
**From your computer.** Once you've created your account, you'll access the User Portal. Under the Home tab, you'll see your Access Code. When you print something from your computer, you can use this code directly on the printer to view and release your print jobs. Before printing, make sure you've downloaded the printer onto your computer. You can find the download link under "Set up my devices" in the portal. Once installed, the printer will appear in your list of available devices, and you can send your print jobs there.
**From your phone.** You can also print from your mobile device. Download the app (the link is also under "Set up my devices") and connect it to your account. You'll be able to print from both your computer and phone, view your print queue in the app, and adjust things like color, double-sided printing, and number of copies before hitting "Print release."
For more detailed instructions, check the "How to print" section in the User Portal. If something doesn't work or you get stuck, don't worry — just reach out to someone from the THT team and they'll help you out.
## Related Topics
- [Holiday and Leave](holiday-and-leave.md) — leave registration in Exact (regular leave, TfT) and the full sick-leave procedure referenced above.
- [Spending and Contracting](spending-and-contracting.md) — the EUR 0.23/km travel allowance and expense submission in NMBRS.
- [Pension Scheme & Benefits](pension-scheme-and-benefits.md) — benefits overview including travel and phone allowances.
- [How We Work Together](how-we-work-together.md) — the core values (Self-discipline, Trust) that underpin flexible hours and etiquette.

View File

@@ -0,0 +1,385 @@
---
title: Holiday and Leave
category: Anything to do with (holiday) leave
source_files:
- docs/Anything to do with (holiday) leave/holiday-vacation-leave.md
- docs/Anything to do with (holiday) leave/sick-leave.md
- docs/Anything to do with (holiday) leave/parental-leave.md
- docs/Anything to do with (holiday) leave/special-leave.md
summary: >
All leave types: holiday/vacation leave (entitlement, registration in Shifts
and Exact, carry-over, public holidays, interchangeable holidays), sick leave
(reporting, Poortwachter, company doctor, wage continuation), parental leave
(maternity, partner/paternity, parental, adoption/foster), and special leave
(study, unpaid, family situations, emergency/short-term/long-term care,
informal care).
tags:
- leave
- vacation
- holiday
- sick-leave
- parental-leave
- maternity-leave
- paternity-leave
- special-leave
- care-leave
- study-leave
- unpaid-leave
- shifts
- exact
- wazo
- poortwachter
related_topics:
- day-to-day.md # Time-for-Time accrual & registration, sickness in Exact
- pension-scheme-and-benefits.md # 25 vacation days, allowances affected by unpaid leave
- how-we-work-together.md # People Officer role, core values
- learning-personal-development.md # study leave relates to development
key_facts:
vacation_days_fulltime: 25 (20 statutory + 5 non-statutory)
mandatory_consecutive_vacation: 2 weeks per year
carry_over_hours_fulltime: 80
statutory_days_expiry: 6 months after the accrual calendar year
non_statutory_days_expiry: 5 years
leave_admin_tool: Microsoft Shifts (requests) + Exact (balance)
sick_report_window: 08:3009:00 by phone to People Officer
wage_continuation_illness: 100% year 1, 70% year 2
maternity_leave_weeks: 16 (20 for multiple births)
partner_birth_leave: 1 week paid (100%) + 5 weeks additional (70% via UWV)
parental_leave_per_child: 26 x weekly hours (9 weeks paid at 70% before age 1)
study_leave_days: 5 per year (40h week, pro-rata), expire 31 December
unpaid_leave_max: 12 months (permanent contracts)
interchangeable_holidays_per_year: 4
ai_context: >
The "People Officer" is the Holacracy role that approves and administers all
leave (see who-we-are.md / how-we-work-together.md). Leave REQUESTS are made in
Microsoft Shifts; leave BALANCES and absence are administered in Exact.
Time-for-Time (TfT) registration steps are documented canonically in
day-to-day.md (Overtime) and only referenced here. Parental-leave rules align
with the Dutch Work and Care Act (Wet Arbeid en Zorg / WAZO); care-leave rules
also derive from WAZO. Unpaid leave and several care leaves reduce untaxed
allowances and can affect pension and the annual salary raise — cross-reference
pension-scheme-and-benefits.md and learning-personal-development.md (Growth &
Reward).
---
# Holiday and Leave
This section covers every form of leave at Respellion: holiday/vacation leave, sick leave, parental leave, and special leave (study, unpaid, family/care situations).
> **Where to request and register leave:** Leave requests are made in **"Shifts" within Microsoft Teams** and approved by the **People Officer**; balances and absence are administered in **Exact**. This applies to all leave types below, including sick leave.
## Holiday and Vacation Leave
### Leave procedure
When you take leave, you'll of course have a critical look at your calendar and your colleagues and make sure that your colleagues know that you are not there before you take leave. The administration is done in "Shifts" within Microsoft Teams.
Make a new Request for Time-Off in Microsoft Shifts with the right category. This also includes Sick Leave. Your request will be approved by the People Officer, after which the leave will be visible in Shifts to the whole team.
Create an event in your own calendar for your holiday to ensure your agenda is blocked.
If you are not sure whether your leave falls under holiday hours but, for example, under care or a sudden absence, please contact the People Officer role so together we can look at the possibilities. Also see the **Sick Leave** and **Special Leave** sections below for more info.
### Leave administration in Exact
**1. Basic Principles**
- **Time sheets are for productive hours:** in the time-sheet administration you only register productive hours (clients, internal projects, work meetings, or business development). Absences such as leave should **not** be registered here.
- **Leave administration for absences:** all forms of absence — regular leave, special leave, and time-for-time — are registered via the leave registration module in Exact.
- **Time-for-Time (TfT):** if you work more hours than your contract stipulates in a month, these extra hours will be added to your TfT balance at the beginning of the following month.
**2. How to Register Regular Leave**
1. In Exact Online, navigate to the **Leave Registration** module.
2. Click on **"Create leave registration"**.
3. Select your own name.
4. Under **Leave Type**, choose **"Verlof" (Leave)**.
5. In the **Description field**, note the dates and the number of hours you wish to take off.
6. Upon approval, this will be automatically deducted from your leave balance (statutory hours are used first, followed by non-statutory hours).
**3. How to Register Time-for-Time (TfT)**
TfT accrues at the beginning of each month (the previous month's overtime is added as a separate balance) and is taken via the same Leave Registration flow, choosing leave type **"Tijd-voor-tijd"**; ensure you do not create a negative TfT balance. The full TfT accrual logic and compensation rates (weekday 100%, weekend 175%, public holidays 300%) are documented in [Day to Day → Overtime and Flexible Hours](day-to-day.md).
### Vacation leave
If you work full-time, you are entitled to **25 vacation days**. Of these, 20 are statutory and 5 are non-statutory. If you work part-time, this is calculated based on the amount of time you work. The same applies when you start or stop employment mid-year.
We think it's important to chill for a considerable amount of time during the year — and when we say this, we really mean relax and not think about work. That's why you must take **two consecutive working weeks of vacation a year**.
### How to take (annual) leave
You need to apply for leave in advance, in "Shifts" within Microsoft Teams. Coordinate within your circle who is on holiday and when, and make sure that important things or certain tasks are transferred to your colleagues during the time when you're on vacation.
When you become ill unexpectedly during your vacation, you will keep your vacation days. Read more in the Sick Leave section below.
### Vacation days that have not been used
If by the end of the year you haven't used up your full vacation hours, you can bring **80 hours** of leave (on a full-time basis) to the new year. The **statutory days expire 6 months** after the calendar year in which you accrued them. For example, the vacation days you received in 2024 but did not use expire on July 1st 2025.
- An exception can be made if you were chronically ill.
- **Non-statutory** supplementary days expire after **5 years**.
We find it important that you get days off; therefore it is not intended to save up leave time. That is why we don't pay in exchange for days off.
An upper and lower limit of the leave balance is set on January 1st of each year. On January 1st, a negative balance of one week of contracting hours is permitted. On January 1st, the number of extra non-statutory vacation hours may not exceed twice the amount of contracting hours per week. When the minimum or maximum is reached, taking or saving leave hours is only permitted again after leave hours have been added to or taken from the balance. In the case of leave exceeding the upper limit at the end of the calendar year, agreements will be made to reduce the credit as soon as possible.
### Public Holidays
On the following days you get a day off:
- New Year's Day (January 1st)
- Easter Monday
- King's Day (April 27)
- Liberation Day in the anniversary year (May 5th, 2025/2030)
- Ascension Day
- Whit Monday
- Both Christmas Days (December 25th and 26th)
You may also leave a bit earlier (one hour earlier) on:
- Good Friday
- 5th of May in the non-lustrum years
- the day before Christmas
- New Year's Eve
This is a small compensation for those who are working these special days. So taking the day off will cost you the normal 8 hours. There is no extra compensation for public-holiday leave that falls on your standard schedule-free day in case of part-time employment.
The days of public holidays are by default greyed out in Exact. However, you do need to register holiday leave on this day; and if you do need to work on these days, you can register those hours on your project.
### Interchangeable Holiday Policy
We want to be an inclusive organization, so we have the Interchangeable Holiday Policy. With this policy, you may exchange **4 national Dutch holidays per year** for any other day of your choice. By doing this, we hope to offer a more inclusive arrangement for colleagues who, for example (but not limited to), prefer to celebrate Eid al-Fitr (also referred to in the Netherlands as Suikerfeest), Thanksgiving, Yom Kippur, your birthday, or Christmas Eve.
If you want to take another day off instead of the regular public holidays, you request a day of leave with "verlofsoort" **FEESTDAG**.
## Sick Leave
Respellion wishes to provide a good physical and psychological working environment and strives to make the workplace attractive for the individual, as well as to prevent absence due to illness, by offering an exciting daily work life with unique development opportunities. We believe that open and honest communication helps to reduce absenteeism and supports and retains long-term employees, even when dealing with difficult and sensitive issues.
The sickness and absence policy is made in respect of the employees on leave and the overall operation of the company, in order to accommodate everyone. We all have an obligation to be aware of each other's mental and physical well-being, so it is important that we help each other. Respellion has relevant and competent experience in this area and will support the employee as much as possible, while the individual employee is respectfully obligated to keep Respellion informed.
According to the (Dutch) Wet Verbetering Poortwachter (WVP), the employee and employer are jointly responsible for resuming work as quickly as possible in the event of incapacity for work due to illness. A few points are explained briefly below. We have a separate Absenteeism Protocol that describes this more extensively.
### Reporting sick
If you want to call in sick, contact the People Officer role by phone in the morning between **08:30 and 09:00**. During the phone call, this colleague will ask you when you think you will be better and discuss what you are working on and what may need to be transferred. The People Officer role will communicate your absence in Microsoft Teams.
If you become ill during the working day and want to go home, report this personally to the People Officer role. When you have recovered, we kindly ask you to call the People Officer role.
(You do not register sick hours in Exact yourself — see [Day to Day → Sickness registration](day-to-day.md).)
### During your sickness
When you are sick, make sure you are available on your phone so that we or the external health service (Arbodienst) can reach you. We will keep in touch to hear how you're doing. When your nursing address changes, you must also inform us.
The Wet Verbetering Poortwachter sets out the efforts both you and Respellion need to make to speed up your recovery. The Absenteeism Protocol has more information — make sure you read this when you are sick.
### Company doctor
If you are sick for approx. five days, the Medical Case Manager of the Health and Safety Provider named **GOED** will probably contact you. During this conversation, questions will be asked regarding your health issues. Expect questions about prescribed medications, physical/mental struggles, or issues related to work or private life that could affect your health. Try to answer the questions as best you can so the medical case manager can help and guide you accordingly. This call will take place unannounced.
When you have been ill for (almost) 6 weeks, a next appointment will be scheduled so that a Problem Analysis can be made. A written report will follow; ask the contact person at GOED for log-in details so you can view the Problem Analysis.
Do you dread it? Don't worry — all these steps are here to help and support you. Feel free to contact the People Officer when you are nervous about this. Together you can prepare for this conversation so any doubts or insecurities can be addressed before the scheduled appointment with the Medical Case Manager.
▶️ Last but not least: all medical conversations with GOED are confidential; the employer will not receive any details.
### Obligation to continue payment of wages
If you cannot work due to illness, we continue payment of the salary for the **first year in full**. In the **second year, 70%** is paid.
### Sick during holidays
If you become ill during your holiday, you must inform us as soon as possible (by telephone), stating your holiday address. After your return, you must be able to submit a 'medical certificate' (drawn up by a doctor during the illness) to the occupational health and safety service. This certificate states the duration, nature and treatment of the incapacity for work. Based on this declaration, the occupational health and safety service will advise Respellion on the return of holidays.
### On holiday during illness
If you want to take time off during a period of illness to go on holiday, you do so in consultation. Sometimes you will need a 'statement of no objection' from the occupational health and safety service. With this statement, you can submit a request to be allowed to go on holiday. We will decide whether to grant the holiday. Your holiday should never be an obstacle to your recovery.
### Taking leave during reintegration
When taking leave, colleagues who can partly perform their own work will have to deduct the days of leave for the entire working day/week from their leave balance. This is because you have also accrued the leave balance over the entire working hours. For example: you have a 36-hour contract and are 50% incapacitated for work, so you are currently working 18 hours a week temporarily. If you want free time during this reintegration, you must apply for 36 holiday hours.
## Parental Leave
This policy outlines the parental leave entitlements for Respellion, aligned with current Dutch regulations as stipulated in the Work and Care Act (Wet Arbeid en Zorg).
### Maternity Leave
**Entitlement.** Female employees are entitled to **16 weeks** of fully paid maternity leave. For multiple births (e.g., twins or triplets), this is extended to **20 weeks**.
**Breakdown:**
- Pregnancy Leave (Zwangerschapsverlof): 46 weeks before the expected due date.
- Maternity Leave (Bevallingsverlof): 1012 weeks after childbirth.
**Important Points:**
- It is mandatory to take at least 4 weeks of leave before the expected due date.
- If an employee chooses to take less than 6 weeks before the due date, the remaining days are added to the maternity leave period.
- If the baby is born later than the due date, the period between the due date and the actual birth date is added to the total leave period.
- If the employee becomes ill due to pregnancy before maternity leave is due to start, maternity leave may automatically commence 6 weeks before the estimated delivery date.
- If the baby is hospitalized immediately after birth, maternity leave starts the day after the mother and baby leave the hospital.
- During maternity leave, employees receive 100% of their salary.
- Employees must inform their employer about their intention to take maternity leave at least 3 weeks in advance.
- Holiday hours continue to accrue during maternity leave.
### Partner / Paternity Leave
**Entitlement.** Partners (including same-sex partners) of women who have given birth are entitled to:
- 1 week (5 working days) of fully paid birth leave (geboorteverlof).
- 5 weeks of additional birth leave (aanvullend geboorteverlof) at 70% of their salary.
**Important Points:**
- The initial 1 week of leave must be taken within the first 4 weeks after the birth.
- The additional 5 weeks must be taken within the first 6 months after the birth.
- The additional leave can be taken all at once or spread out over the 6-month period.
- To take the additional leave, employees must submit a request via the regular leave request procedure at least 4 weeks in advance.
- Employers can only adjust the timing if the leave would cause serious organizational problems, and must finalize any changes no later than 2 weeks before the scheduled leave.
- During the additional leave, the UWV pays 70% of the employee's salary (up to the maximum daily wage).
- Holiday hours continue to accrue during partner leave.
### Parental Leave
**General Information:**
- Parental leave is available for all employees who are parents or legal guardians of children under the age of 8.
- Each parent is entitled to 26 times their weekly working hours of parental leave per child. For example, an employee working 40 hours per week is entitled to 1,040 hours (26 × 40) of parental leave per child.
- Parental leave applies to each child separately. For multiple births or adoptions, the entitlement applies to each child.
- Parents with a "family relationship" with the child (biological parents, adoptive parents, foster parents, and step-parents who live with and take care of the child) are eligible.
**Unpaid Parental Leave:**
- Can be taken until the child reaches the age of 8.
- The leave can be taken in one block or spread out over time.
- No vacation hours are accumulated over the unpaid parental leave.
- If an employee falls ill during parental leave, the leave continues as normal.
- Employees should discuss how they wish to implement the leave with the People Officer at least 2 months before the desired start date.
**Paid Parental Leave:**
- Of the total 26 weeks of parental leave, 9 weeks can be taken as paid parental leave.
- The paid leave must be used before the child reaches the age of 1 year.
- During paid parental leave, the employee receives a benefit of 70% of their daily wage.
- This benefit is applied for by the employer to the UWV and the salary is adjusted accordingly.
- The benefit can only be applied for entire work weeks.
- Vacation hours are accumulated over the paid parental leave.
- All other conditions that apply to unpaid parental leave also apply to paid parental leave.
### Application Process
**For Maternity Leave:**
1. Inform the People Officer of the pregnancy and intended maternity leave at least 3 weeks before the leave starts (preferably earlier).
2. Provide a pregnancy declaration letter (zwangerschapsverklaring) from your midwife or gynecologist.
3. The employer will complete a WAZO form (maternity and childbirth benefit form) and send it to the UWV.
**For Partner / Paternity Leave:**
1. For the initial week of leave, notify your employer as soon as possible after the birth.
2. For the additional 5 weeks, submit a request via the regular leave request procedure at least 4 weeks in advance.
**For Parental Leave:**
1. Submit a written request via the regular leave request procedure at least 2 months before the desired start date.
2. Specify how you wish to take the leave (all at once or spread out).
3. After consultation, the employer will confirm the leave arrangements in writing.
**For Adoption or Foster Care Leave:**
1. Notify your employer at least 3 weeks before you want the leave to start.
2. Indicate whether you want to take your leave all at once or spread it out.
## Special Leave
### Study leave
For every year, you are entitled to **5 days of study leave** based on a 40-hour work week, calculated pro rata if you work part-time. These expire on **31 December**. You can use the study leave if your selected training takes place during working hours. Apply via the request-for-leave procedure.
### Unpaid leave
Employees with a permanent contract may apply for unpaid leave for up to **12 months** for educational purposes, travel, or other special activities requiring a longer leave of absence. This can also be used for short-term leave such as extra vacation. The following rules apply:
- Vacation days must be used first in order to request unpaid leave.
- Applications are submitted to the People Officer role.
- Provide as much notice as possible of (long-term) sabbatical leave — at least 3 months before the preferred start date.
- No salary is paid during the agreed period, and benefit arrangements can be suspended.
- During unpaid leave you do not build up holiday pay, holiday hours, unemployment rights, or pension. Read more on the Rijksoverheid webpage.
- If the employee falls ill during the leave, they are not entitled to continued payment of salary during the period of unpaid leave.
- Up to one month of unpaid leave, the employer keeps paying for the pension scheme. After one month, the pension scheme is not paid until the employee resumes daily work.
- In case of a longer period of unpaid leave, you do not receive a travel allowance (nor an internet allowance or other related allowances).
- Taking unpaid leave could affect your annual salary raise.
- If you take unpaid leave for two months or more across the calendar year (in addition to regular and purchased holiday leave), the standard salary raise will not be implemented. As a result, you will not qualify for an annual salary raise if your leave extends to 3.5 months or more, encompassing both paid and unpaid leave periods.
> Pension and allowance impacts above relate to [Pension Scheme & Benefits](pension-scheme-and-benefits.md); the salary-raise impact relates to [Learning & Personal Development → Growth & Reward](learning-personal-development.md).
### Special leave for activities and (family) situations
There may be special situations where you are not able to work. In these cases you are entitled to special leave with full pay. Common sense applies: if, for example, you are invited to a wedding reception only at the end of the day, you still work in the morning. Apply via the regular request-for-leave procedure.
| Situation | Maximum special leave |
| --- | --- |
| Deposition for marriage | 1 day |
| Marriage or registered partnership of yourself | 2 days |
| Marriage or registered partnership of a child, brother, sister, parent, parent-in-law, brother-in-law or sister-in-law, provided that the ceremony is attended | 1 day |
| Moving house | 1 day per calendar year |
| Paternity leave | 1 workweek and max 5 weeks additional |
| Spouse/partner, relatives by blood or marriage of the 1st degree passing away | From the day of death until the day of the funeral or cremation |
| Relatives by blood or marriage of the 2nd degree passing away | 2 days: the day of death and the day of the funeral or cremation |
| Relatives by blood or marriage of the 3rd and 4th degree passing away | The time to attend the funeral or cremation |
| Necessary doctor's visit which cannot take place outside of working hours | Max 8 hours on a yearly basis, see explanation below |
| 12.5 / 25-year relationship with your partner | 1 day |
### Doctor's visit
A doctor's visit means a visit to the doctor, dentist, a specialist, or a therapist to whom you are referred. You plan it in your own time; if that is not possible, consult with your circle about convenient times (e.g. beginning or end of the working day). We continue payment of salary for the time needed for the doctor's visit, within the working day, to a maximum of **two hours per visit**. For seeing a **specialist** there is a maximum of **four hours per visit**. You will work the remaining hours before or after the visit. The maximum number of hours reimbursed cannot exceed **8 on an annual basis**. In special cases there can be a deviation, in consultation with the employer.
### 'Other' Special Leave Situations — non-family
Other personal situations, like loved ones passing away or a marriage/anniversary of non-family members. Happy or unpleasant things can also happen to non-family members, so we want to provide leave for these situations. The assessment of whether you make use of these days is up to you, but please consult the People Officer role upfront. Some examples: marriage or funeral of a person that is important to you.
### Maternity / Paternity / Parental Leave
See the **Parental Leave** section above.
### Emergency, short-term and long-term care-related leave
Working flexibly is very normal in our organization. You get the opportunity to organize your own working hours according to your own preferences. Working from home, even in the evenings, is no problem. So if you cannot work during the daytime for some reason, it is fine with us if you finish it at another time. We give you a lot of freedom and responsibility and trust that you know how to use it well.
If there are urgent private circumstances of a very serious nature, you may be eligible for emergency leave, short-term medical care leave, or long-term care leave. Our starting point is the legal framework of the Work and Care Act (Wet Arbeid en Zorg). We would like to look at the possibilities with you.
You report the situation of force majeure immediately to the People Officer role. If this is not possible, report it as soon as possible. We may require that you demonstrate that you really haven't been able to work as a result of one of the given reasons — for example, by submitting a medical certificate. In addition, we keep track of these hours.
| | Emergency | Short term | Long term |
| --- | --- | --- | --- |
| Keywords | unforeseen, serious, short-term (such as arranging care) | you're the only one who can care for your very sick (life-threatening) child, spouse, or parent | for necessary care for others, in case of serious illness |
| Example | picking a sick child up from daycare; waiting for the plumber to fix a burst water pipe | helping partner at home with medical tasks when they just had surgery | caring for a terminally ill partner |
| Payment | Paid leave, 100% | Paid leave, 70%, fully supplemented | Unpaid leave |
**Emergency leave.** Applicable if there has been an unforeseen emergency with the employee that cannot be delayed — for example a sudden hospitalization of an immediate family member, or your partner giving birth. The length depends on the situation but presumably cannot be longer than one day. Emergency leave is intended for initial care and arranging a structural solution. We expect you to go back to work as soon as the emergency has been resolved. It is possible that emergency leave becomes short-term care leave. We are obligated as an employer to pay the full salary during emergency leave. *(Keywords: unforeseen, serious, short-term such as arranging care.)*
**Short-term medical care leave.** You may request short-term care leave if it is necessary to care for a sick partner or resident children. It applies when you are the only person who has to take care of an immediate family member (child, spouse, or parent). Short-term care takes up to twice the hours worked in a week — so 80 hours per year for a full-time workweek. You can also take days alternately. By law we are required to pay 70% of the salary; we choose to pay your salary fully during short-term care leave. If the period of short-term care is not enough, you can also take unpaid long-term care in case of a very serious illness. The employer may ask for a reason for the leave once you return. Care leave may be refused if there are valid reasons (for example, if the company will get in trouble because of the absence). *(Keywords: you're the only one who can care for your very sick (life-threatening) child, spouse, or parent.)*
**Long-term care leave.** Possible in case of a seriously ill spouse, child, or parent. From July 1, 2015, you can also take long-term care leave for necessary care (helplessness or illness) of grandparents, grandchildren, brothers/sisters, other housemates, and people with whom you have a social relationship and who are dependent on your help. Salary is not continued during long-term care leave. The employer may only refuse permission in case of substantial business interests. You can take **6 weeks of long-term care leave a year, distributed over 18 weeks**. *(Keywords: unpaid, for necessary care for others, in case of serious illness.)*
> **WAZO definitions (apply to both short-term and long-term care leave):**
> - *Life-threatening illness*: a state of health so serious that, according to objective medical standards, the person's life is in serious danger in the short term.
> - *"Needy" (in need of assistance)*: the condition of a person whereby they require assistance for self-reliance, participation, protected living, or shelter that is not provided within the framework of a professional providing assistance and that exceeds the usual assistance.
> Taking emergency/care leave will affect the amount of your untaxed allowances such as the home-working allowance and possibly internet or telephone allowance (see [Pension Scheme & Benefits](pension-scheme-and-benefits.md) and [Spending and Contracting](spending-and-contracting.md)).
### Informal care ('mantelzorg')
The official government definition: caregivers provide unpaid care for a chronically ill, handicapped, or helpless partner, parent, child, or another family member, friend, or acquaintance for more than 8 hours per week and/or more than 3 months. A caregiver didn't choose to provide care — it happens to someone because that person has an emotional attachment to the person who needs care.
Practice has shown that care and work can be combined, as long as it is managed correctly — both by the organization and by you. This way, overload can be prevented and caregivers keep both their work and family responsibilities well balanced. Research shows that giving visibility to informal care and discussing it can provide very good solutions. Are you a caregiver? Then talk about it with your colleagues and the People Officer (Request for Leave) role, so together we can look at possible support options in addition to leave, and find workable solutions so that informal care and work can be better combined. Taking this leave will affect the amount of your untaxed allowances such as the home-working allowance and possibly internet or telephone allowance.
## Related Topics
- [Day to Day](day-to-day.md) — Time-for-Time accrual/registration and the rule that sick hours are not self-registered in Exact.
- [Pension Scheme & Benefits](pension-scheme-and-benefits.md) — the 25 vacation days as a benefit, and how unpaid/care leave affects pension and allowances.
- [Learning & Personal Development](learning-personal-development.md) — study leave for development, and the salary-raise impact of extended unpaid leave.
- [How We Work Together](how-we-work-together.md) — the People Officer role and the core values that frame our flexible approach to leave.

View File

@@ -0,0 +1,150 @@
---
title: How We Work Together
category: How we work together
source_files:
- docs/How we work together/our-purpose.md
- docs/How we work together/core-values.md
- docs/How we work together/coaching-culture-faq.md
- docs/How we work together/circles-huddles-celebrations.md
- docs/How we work together/office-information.md
summary: >
Respellion's purpose and impact, the four core values and manifest, the
coaching culture (huddle check-ins, huddle elevators, capacity building, POP),
and practical office information for The Hague.
tags:
- purpose
- core-values
- manifest
- coaching-culture
- huddles
- capacity-building
- personal-development-plan
- office
- open-source
related_topics:
- who-we-are.md # Holacracy model, QMS, services
- learning-personal-development.md # POP detail, Growth & Reward, conferences
- day-to-day.md # office presence, etiquette
key_facts:
purpose: "A society in which open-source technology is used to enable digitization in a sustainable way."
core_values: [Trust, Courage, Self-discipline, Entrepreneurship]
huddle_checkin_frequency: twice a week
huddle_elevator_frequency: every two months
capacity_building_areas: [Professional, Emotional, Physical]
pop_model: Ofman's Core Quadrant
pop_tool: Miro board
office_address: "Waldorpstraat 5, 2521 CA 's-Gravenhage (PostNL building, 1st floor)"
ai_context: >
The coaching culture described here is the SINGLE canonical description for the
whole handbook; the Learning & Personal Development section references it rather
than repeating it. Conflicting source values were resolved: huddle check-ins are
twice a week, huddle elevators every two months, and the capacity building
framework has exactly three areas (Professional, Emotional, Physical). The
coaching culture replaces traditional performance management and complements the
Holacracy governance model (see who-we-are.md); reward is handled in
learning-personal-development.md (Growth & Reward).
---
# How We Work Together
This section covers Respellion's purpose, its four core values and the manifest, the coaching culture (huddles, capacity building, and the Personal Development Plan), and practical office information.
## Purpose and Impact
Respellion provides high-performing and flexible professionals and coaches who understand that sustainable software is only created through close and excellent collaboration. We tell it like it is and do what we promise.
> "A society in which open-source technology is used to enable digitization in a sustainable way."
Respellion is an organisation that has knowledge and experience about technology and organisation. We have a vision of how digital solutions can best be developed technically and how this should be approached from an organisational point of view. This combination is exceptionally strong.
Read more about our knowledge areas on our website.
## Our Core Values and Manifest
### Our core values
We have four core values which form the basis for our organization, the cooperation, and the behavior we strive for. In our application process, we will find out together how these values are expressed in you.
**1 — Trust.** We give trust by allowing room for autonomy. We trust each other and are open and transparent. That's why we talk with each other instead of about each other. We have faith in someone else's intentions, assume these to be right, and express that trust. Everyone can rely on each other, and we stand for results.
**2 — Courage.** We have courage and dare to deviate from the traditional way of working when we see things we can improve. It's okay to make mistakes. In this way, we learn to do (even) better. We dare to make ourselves vulnerable and welcome feedback. We like to do things differently. That makes us a bit rebellious, but we'll always have a good reason for it.
**3 — Self-discipline.** We make clear agreements and honour them. We take responsibility for our activities and set clear priorities. We have the discipline to develop ourselves and want to be the best in our field. Trust requires self-control and the courage to take a step beyond your own ego.
**4 — Entrepreneurship.** We are committed and driven entrepreneurs who think in terms of opportunities and possibilities. For our environment and for ourselves. Because things will change, we are flexible and critical. When we tackle something, we do it with both hands and we feel responsible for the result.
### Respellion manifest
Our values are ultimately expressed in behaviour, which is why we have formulated four statements that our customers can hold us accountable for. "Practice what you preach" is very important to us.
**We are always in motion to be agile.** Society is a system of networks that is always in motion. Success comes from the ability to adapt. Adapting is needed on an organizational and individual level. We want to keep learning and align what we do with each other and society.
**Collaboration based on a common interest.** We believe in cooperation as it is needed in a society — dealing with each other as you do in a village or neighbourhood. Here reliability is a great asset, with attention to the interests of others. Our vision is that there is no individual interest that takes precedence over the collective interest. This requires self-insight and awareness of our values, beliefs and behaviour. That insight is necessary to ensure that we choose the collective instead of following our ego.
**Working with a smile and a caring attitude.** There is no "ladder of success" in our organization. We have a responsibility to take care of each other rather than pursue individual interests. We are a collective of professional, skilled people. We hold each other accountable for this responsibility with a caring attitude, which leads to better cooperation and therefore high-quality services. As a result, we practice what we preach and are congruent in our behaviour.
**Our organisation strives for a balance between people, planet and profit.** We are convinced that sustainable solutions are only possible if attention is paid to the balance between people, planet and profit. We don't think in terms of us and them. Long-term results always outweigh short-term interests. The open-source model fits in seamlessly with this vision.
### Respellion Operating System
We have knowledge and experience in successfully executing result-driven projects and in how to empower people in high-performing teams. Autonomy and craftsmanship are central in our daily activities.
**Agile way of thinking and dynamic structure.** We work with organic, flat-structured teams. We don't have managers: employees take the initiative and responsibility for the autonomy in their work, meaning they choose the roles that they are good at and that motivate them.
In our culture, employees trust each other, demonstrate courage and discipline, and embrace an entrepreneurial mindset. Our roles are described on Glassfrog.
## Coaching Culture, Circles, Huddles and Celebrations
At Respellion, we've evolved beyond traditional performance management to embrace a coaching culture where continuous improvement and collaboration thrive. Our structure is designed to foster regular feedback, support, and guidance among all team members. The coaching culture is the concept in which employees regularly give each other feedback, support and guidance with the aim of continuous improvement and collaboration. It evolved from an initial 'feedback culture' into a more comprehensive coaching approach focused on employee development, and is intended as an alternative to traditional performance management systems.
### Huddle Check-ins
These twice-weekly small group meetings create a supportive environment where team members can share updates, discuss concerns, and offer mutual support. Unlike traditional mentorship programs, these frequent check-ins ensure meaningful connections within trusted, small groups — more frequent and meaningful than traditional mentoring programs.
### Huddle Elevators
These in-depth sessions, held every two months, focus on personal and professional development. The "elevator" concept emphasizes building on your strengths rather than just addressing weaknesses. These sessions incorporate your Personal Development Plan (POP) and competency insights to guide meaningful coaching conversations, and serve as a platform for coaching conversations that require inputs such as personal development plans and competency insights.
### Capacity Building Framework
Our approach to development is holistic and encompasses three key areas, with the goal of creating balanced and satisfied employees who can be fully themselves at work:
- **Professional Capacity:** intellectual and work-related skills — enhancing your work-related skills and intellectual abilities.
- **Emotional Capacity:** social skills, self-reflection, alignment of personal values with work, inclusivity and diversity.
- **Physical Capacity:** encouragement of healthy lifestyle choices.
### Personal Development Plan (POP)
Every employee at Respellion draws up a personal development plan, based on the capacity building framework. A fundamental part of this is identifying core qualities using **Ofman's Core Quadrant model**. This model helps employees understand their strengths (core qualities), pitfalls, challenges, and allergies (what irritates them in others). The plan is visualized on a Miro board for easy updating and sharing during the "elevator" sessions, and is accessible to all colleagues.
### Openness and vulnerability
Openness and vulnerability are seen as essential elements for the success of the coaching culture. Employees are encouraged to be open about their development points and to be vulnerable, so that colleagues can better support and empower them. The "elevator" sessions and the sharing of personal development plans on the Miro board are designed to create a safe environment for this candor. Questioning core qualities in different relationships and reflecting on them in the group also contribute to self-insight and an open dialogue.
### Relationship to Holacracy and performance management
The coaching culture is intended to replace traditional performance management — in which assessment and reward are central — with a more continuous, collaborative and development-oriented approach. While there may be parallels with elements from Holacracy, such as working in smaller groups (huddles), Holacracy focuses primarily on organizational structure and decision-making, whereas Respellion's coaching culture focuses specifically on personal and professional development and on feedback mechanisms within the organization. It is recognized that effective implementation of the huddles may require some form of 'huddle lead' or facilitator, which introduces an element of leadership within the self-organizing teams.
> Reward and salary growth tied to this development model are described in [Learning & Personal Development → Growth & Reward](learning-personal-development.md).
## Office Information
Respellion has an office in the PostNL building at Waldorpstraat 5 in The Hague, near the Den Haag Hollands Spoor station. The building is open from approximately 06:00 to 22:00.
**Lock.** The Respellion office is located on the first floor. The door opens with the Salto KS app (https://saltoks.com/), which you need to install on your smartphone. The People Officer will ensure that the building manager invites you, so you can open the door with your smartphone.
**Meeting Spaces.** The meeting rooms on the first floor can be reserved via The Hague Tech.
**Flex Desks.** We are allowed to use the pink tables on the first floor as flex desks when it gets busy in our office. Please be mindful of the people who rent the flex desks, as a few seats need to remain available for them as well.
**Contact information**
- Address: Waldorpstraat 5, 2521 CA 's-Gravenhage, Nederland
- E-mail: Patrick Smulders (ps@respellion.nl) or Raymond Verhoef (rve@respellion.nl)
- Telephone: +31 (0)85-0607428
## Related Topics
- [Who We Are](who-we-are.md) — the Holacracy model and ISO 9001 QMS that the core values and coaching culture support.
- [Learning & Personal Development](learning-personal-development.md) — the POP in practice, the Growth & Reward model that links development to salary, and conference attendance.
- [Day to Day](day-to-day.md) — office presence registration and office etiquette that complement this office information.

92
knowledge-base/index.md Normal file
View File

@@ -0,0 +1,92 @@
---
title: Respellion Employee Handbook — Knowledge Base Index
category: index
summary: >
Entry point and relationship map for the Respellion Employee Handbook knowledge
base. Lists every category file, what it covers, the canonical owner of each
cross-cutting topic, and the key concept relationships an AI should use to
answer questions and route between documents.
tags:
- index
- map
- knowledge-base
- respellion
- employee-handbook
ai_context: >
Use this file first to decide which category file answers a question and to
understand how topics relate. Each topic has exactly ONE canonical home
(listed under "Canonical owners"); other files reference it rather than
duplicating it. When facts conflict, the values in this knowledge base have
already been reconciled (see "Resolved conflicts"). Glossary terms are
organization-specific and recur across files.
---
# Respellion Employee Handbook — Knowledge Base
At Respellion we build software using open-source technology. We believe in transparency and collaboration — that's why our culture is "open-sourced" and this handbook is publicly available. This knowledge base is a consolidated, deduplicated version of the handbook prepared for retrieval by an AI assistant.
## Category Files
| File | Covers |
| --- | --- |
| [Who We Are](who-we-are.md) | What Respellion does; the Holacracy operating model (roles, circles, tensions); ISO 9001 QMS; first-day onboarding. |
| [Roles & Accountabilities](roles-and-accountabilities.md) | The three circles and every role's purpose & accountabilities (People Officer, Privacy Officer, etc.). Personal names removed. |
| [How We Work Together](how-we-work-together.md) | Purpose; the four core values & manifest; **coaching culture** (huddles, capacity building, POP); office information. |
| [Day to Day](day-to-day.md) | Overtime & flexible hours (**Time-for-Time**); office presence; time registration in Exact; etiquette; travel-time policy; printing. |
| [Security](security.md) | GDPR / personal-data handling & BSN rules; BHV/evacuation (placeholder). |
| [Learning & Personal Development](learning-personal-development.md) | **Growth & Reward** (5 Pillars of Value); conferences. (Coaching culture summarized; canonical in *How We Work Together*.) |
| [Spending and Contracting](spending-and-contracting.md) | Spending & purchasing policy; expense categories; **travel km allowance**; submission via NMBRS; WKR rules. |
| [Pension Scheme & Benefits](pension-scheme-and-benefits.md) | Secondary benefits overview; detailed pension scheme (a.s.r. Doen Pensioen). |
| [Holiday and Leave](holiday-and-leave.md) | Holiday/vacation; sick leave; parental leave; special leave (study, unpaid, care). |
## Canonical Owners (single source of truth)
To avoid duplication, each cross-cutting topic lives in exactly one file; all others link to it.
- **Coaching culture, huddles, capacity building, POP** → [How We Work Together](how-we-work-together.md)
- **Reward / salary growth** → [Learning & Personal Development](learning-personal-development.md)
- **Time-for-Time (TfT) accrual & overtime rates** → [Day to Day](day-to-day.md)
- **Travel kilometer allowance (EUR 0.23/km) & expenses** → [Spending and Contracting](spending-and-contracting.md)
- **Travel-time compensation (1.5h threshold)** → [Day to Day](day-to-day.md)
- **Vacation entitlement & carry-over rules** → [Holiday and Leave](holiday-and-leave.md)
- **Benefits at-a-glance & pension detail** → [Pension Scheme & Benefits](pension-scheme-and-benefits.md)
- **Personal-data / GDPR handling** → [Security](security.md)
- **Role structure / who-does-what (by role, not person)** → [Roles & Accountabilities](roles-and-accountabilities.md)
## Key Concept Relationships (for the AI)
- **Holacracy → everything.** The Holacracy model (roles, circles, tensions, Lead Link, Facilitator, Tactical/Governance meetings) defined in *Who We Are* is the vocabulary used across all files. *Roles & Accountabilities* is the concrete map of every role; the **People Officer** role administers leave, sickness reporting, onboarding, the performance model, and many requests, while the **Privacy Officer** owns GDPR handling and **Process Guardian** owns the QMS/ISO 9001 audits.
- **Core values → policies.** The four core values (Trust, Courage, Self-discipline, Entrepreneurship) in *How We Work Together* are cited as the rationale for flexible hours, spending autonomy, the coaching culture, and office etiquette.
- **Coaching culture ↔ reward.** *How We Work Together* owns the development half (huddles, capacity building, POP); *Learning & Personal Development* owns the reward half (5 Pillars, salary banding). Self-evaluation happens during the bi-monthly **Huddle Elevator**.
- **QMS ↔ practice.** The ISO 9001 QMS in *Who We Are* maps abstract clauses onto concrete practices documented elsewhere: "people are our most important capital" → coaching culture & Growth & Reward; "security by design" → *Security*; DoD/CI-CD → engineering practice.
- **Leave ↔ benefits ↔ money.** Vacation days appear as a benefit (*Pension & Benefits*) but are detailed in *Holiday and Leave*. Unpaid and care leave reduce untaxed allowances (*Spending and Contracting*), affect pension (*Pension & Benefits*), and can affect the annual salary raise (*Learning & Personal Development*).
- **Travel: two different things.** Travel **time** (compensated above 1.5h one-way) lives in *Day to Day*; the travel **kilometer allowance** (EUR 0.23/km) lives in *Spending and Contracting* and is summarized in *Pension & Benefits*.
- **Exact vs Shifts.** Leave **requests** are made in Microsoft **Shifts**; leave **balances**/absence and **time registration** are in **Exact**. TfT is registered in Exact but its rules live in *Day to Day*.
## Resolved Conflicts
The source documents contained contradictions; this knowledge base uses these reconciled values throughout:
- **Huddle Check-ins:** twice a week.
- **Huddle Elevator sessions:** every two months.
- **Capacity Building Framework:** three areas — Professional, Emotional, Physical (the alternate four-area "Intellectual/Spiritual" framing was dropped).
## Known Gaps
- **BHV & evacuation plan / RI&E** ([Security](security.md)) is a placeholder; the actual procedures still need to be written.
## Glossary (organization-specific terms)
- **Holacracy** — governance framework for self-organization without managers; basis of the Respellion model.
- **Role / Circle / Tension** — Holacracy units: a role has accountabilities, circles group roles, a tension is a gap between current and desired reality.
- **Lead Link / Facilitator** — roles responsible for strategy/prioritization and process oversight respectively.
- **People Officer** — role that handles leave approval, sickness reporting, onboarding, and many HR-style requests.
- **Huddle Check-in / Huddle Elevator** — twice-weekly support meetings and bi-monthly development sessions.
- **POP** — Personal Development Plan, based on Ofman's Core Quadrant, shared on Miro.
- **Time-for-Time (TfT) / "Tijd-voor-tijd"** — saved overtime hours taken later as leave.
- **WKR (Werkkostenregeling)** — Dutch work-related costs tax scheme constraining reimbursements.
- **WAZO (Wet Arbeid en Zorg)** — Dutch Work and Care Act governing parental and care leave.
- **Wet Verbetering Poortwachter (WVP)** — Dutch law on joint employer/employee duties during illness.
- **GOED** — the Health and Safety Provider (Arbodienst) handling company-doctor contact.
- **BSN (Burgerservicenummer)** — Dutch citizen service number; tightly regulated personal data.
- **Glassfrog / Microsoft Loop / Exact / Shifts / NMBRS** — tooling for governance, meeting notes, finance & time, leave requests, and expenses respectively.

View File

@@ -0,0 +1,108 @@
---
title: Learning & Personal Development
category: Learning & personal development
source_files:
- docs/Learning & personal development/coaching-culture.md
- docs/Learning & personal development/growth-reward.md
- docs/Learning & personal development/conferences.md
summary: >
How development translates into reward (the 5 Pillars of Value and the
self-evaluation → salary increase → banding process) and how conference
attendance works. The coaching culture itself is documented canonically in
How We Work Together.
tags:
- growth
- reward
- salary
- performance
- evaluation
- conferences
- professional-development
- coaching-culture
related_topics:
- how-we-work-together.md # canonical coaching culture, huddles, POP
- pension-scheme-and-benefits.md # 40h dev/conference time as a benefit
- spending-and-contracting.md # how to request/expense a conference
key_facts:
conference_hours_per_year: ~40 (full-time)
pillars_of_value:
A_basic_experience_expertise: 100%
B_role_fulfilment_responsibility: 75%
C_impact_results: 80%
D_development_learning_curve: 100%
E_culture_collaboration: 80%
q4_start_rule: No salary increase at next year-turn if you started in Oct/Nov/Dec.
ai_context: >
The Growth & Reward model is the REWARD half of the coaching culture; the
development half (huddles, capacity building, POP, Ofman's Core Quadrant) is
documented canonically in how-we-work-together.md and only summarized here to
avoid duplication. Self-evaluation on the 5 Pillars happens during the Huddle
Elevator sessions (every two months). The ~40 conference hours appears here as
a process and in pension-scheme-and-benefits.md as a benefit; requesting and
expensing a conference is covered in spending-and-contracting.md.
---
# Learning & Personal Development
This section covers how personal development is rewarded (Growth & Reward) and how conference attendance is arranged. The coaching culture that frames all of this — huddle check-ins, huddle elevators, the capacity building framework, and the Personal Development Plan (POP) — is documented canonically in [How We Work Together → Coaching Culture](how-we-work-together.md).
## Coaching Culture (summary)
Respellion has moved beyond traditional performance management to a coaching culture of continuous feedback and development, built on **twice-weekly Huddle Check-ins**, **Huddle Elevator** sessions **every two months**, a three-area **Capacity Building Framework** (Professional, Emotional, Physical), and a **Personal Development Plan (POP)** based on **Ofman's Core Quadrant model** and shared on a Miro board. For the full description, see [How We Work Together → Coaching Culture](how-we-work-together.md). The reward side of this model is described below.
## Growth & Reward
At Respellion, we believe in a fair reward structure that is as transparent and enterprising as our way of working. This also means the model can be adapted if there is a reason to do so. We move away from rigid job scales and instead focus on your actual added value.
### The 5 Pillars of Value
Your impact — and therefore your potential salary growth — is measured on five core pillars. These form the foundation of your development and evaluation. The pillars are **weighted differently**, which determines your final increase category; not every element carries the same weight in the final score.
- **A. Basic Experience & Expertise (Weighting: 100%):** your fundamental professional knowledge and skills.
- **B. Role Fulfilment & Responsibility (Weighting: 75%):** how much ownership you show in your Holacratic roles.
- **C. Impact & Results (Weighting: 80%):** the tangible value you deliver for clients and our purpose.
- **D. Development & Learning Curve (Weighting: 100%):** your growth on intellectual, emotional and physical levels (linked to your Personal Development Plan).
- **E. Culture & Collaboration (Weighting: 80%):** your contribution to the team, living our values, and 'working with a smile'.
Check our anchor descriptions of these five Pillars of Value.
### From Evaluation to Reward
Your salary increase is not a predetermined fact based on years of service, but a reflection of your performance over the past year.
**Step 1: Self-Evaluation.** You assess yourself on the five pillars above; this forms your performance score. Your colleagues will directly challenge you to ensure a complete and honest self-assessment. This feedback takes place multiple times a year during the Huddle Elevator sessions.
**Step 2: Salary Increase.** The determined performance score is linked to a linear model of increase percentages. We use several bands, e.g., 02%, 24%, etc. The higher the performance score, the higher the salary increase. This ensures that above-average performance is rewarded above average, rather than a standard 'automatic' increase for everyone.
When determining the bands, we consider factors such as:
- **Financial health:** we analyse the total increase in wage costs in relation to our turnover and margins. We want to remain a healthy company; steep increases must be covered by our rates and results.
- **Inflation (CPI):** we look at the Consumer Price Index of the past year to protect purchasing power.
- **Market conformity:** we compare our salaries with trends in the IT sector and collective labour agreement wage developments to remain competitive.
The calculated increase is adjusted based on how long you have been employed during the relevant year (seniority). To keep things fair for everyone, in your first year we take into account how long you have been working with us.
- **Pro-rata increase:** if you started later in the year, any salary increase is calculated proportionally to the number of months you have been employed. Someone who has contributed for 12 months receives a larger share of the increase than someone who has contributed for 4 months.
- **Start in Q4:** if you started in October, November or December, you will not be eligible for a salary increase at the next turn of the year. We have not had enough time to properly assess your performance, and you are likely still in your induction period. All colleagues do, however, participate in the Huddle Elevator to gain experience with this assessment model.
**Step 3: Banding.** These scores are validated across the Huddles to form a fair picture. Here we look not only at the numbers but especially at the story behind them. We filter out subjectivity as much as possible to arrive at an objective performance score. Finally, we line up all the new salaries: does the distribution feel logical? Is the ratio between juniors, mediors and seniors still correct? Do the differences between colleagues with similar impact make sense? Where necessary, we make manual corrections to prevent imbalances.
## Conferences
Respellion encourages professional development through conference attendance. Each employee is entitled to approximately **40 hours per year** for attending conferences.
### How to register
**Find Available Conferences.** All available conferences are listed in the SharePoint Events Calendar. You can filter the view to see only tech conferences.
**Express Your Interest.** Indicate your interest by adding your name to the "interest list" in the calendar. Regular reminders will be sent about upcoming conferences.
**Selection Process.** Conference attendance will be determined based on expressed interest. We aim to coordinate attendance so team members can attend conferences together when possible.
For any questions about conference attendance, please contact the People Officer.
## Related Topics
- [How We Work Together](how-we-work-together.md) — the canonical coaching culture, huddles, capacity building, and Personal Development Plan that Growth & Reward builds on.
- [Pension Scheme & Benefits](pension-scheme-and-benefits.md) — the ~40 hours of professional development / conferences listed as a secondary benefit.
- [Spending and Contracting](spending-and-contracting.md) — how to request and expense conferences, courses, books, and certifications.

View File

@@ -0,0 +1,118 @@
---
title: Pension Scheme & Benefits
category: Pension scheme & Benefits
source_files:
- docs/Pension scheme & Benefits/secondary-employment-benefits.md
- docs/Pension scheme & Benefits/jouw-pensioen.md
summary: >
Overview of secondary employment benefits (vacation, holiday allowance, travel
and phone allowances, professional development, WIA disability insurance,
pension) and the detailed pension scheme (a.s.r. Doen Pensioen defined
contribution, contribution calculation, life cycles, survivor/disability cover).
tags:
- benefits
- pension
- defined-contribution
- asr-doen-pensioen
- holiday-allowance
- travel-allowance
- phone-allowance
- wia
- disability-insurance
related_topics:
- holiday-and-leave.md # vacation days detail, unpaid-leave pension impact
- spending-and-contracting.md # travel km allowance detail
- learning-personal-development.md # 40h development/conferences detail
- day-to-day.md # travel allowance vs travel time
key_facts:
vacation_days_fulltime: 25 (200 hours)
holiday_allowance: 8% of gross annual salary (paid monthly pro-rata)
travel_allowance: EUR 0.23 per kilometer
professional_development_hours: 40 per year
mobile_phone_allowance: EUR 35 per month
pension_provider: a.s.r. Doen Pensioen (advice via Zuyderleven)
pension_type: Defined Contribution (beschikbare premieregeling)
pension_premium: 10% of pension base
pension_split: 50% employee / 50% Respellion
pension_franchise_2026: EUR 19172.00
pension_max_salary: EUR 137800.00
partner_pension: 30% of salary (for life, risk basis)
orphan_pension: 20% of salary until age 25
ai_context: >
This file holds both the at-a-glance benefits overview AND the detailed pension
scheme. Several overview items are documented in depth elsewhere and are
cross-linked rather than re-explained: vacation days (holiday-and-leave.md),
the EUR 0.23/km travel allowance (spending-and-contracting.md), and the 40
development hours / conferences (learning-personal-development.md). Pension is
built only on the pension base (salary minus the franchise/threshold, up to the
max salary), because AOW (state pension) covers the lower part. Unpaid leave
affects pension accrual — see holiday-and-leave.md.
---
# Pension Scheme & Benefits
This section gives an at-a-glance overview of secondary benefits and then details the pension scheme.
## Secondary Employment Benefits (Overview)
At Respellion, we believe in taking good care of our team. Below is a summary of the secondary benefits you are entitled to:
- **Vacation Days:** **25 vacation days** (200 hours) per year based on full-time employment. (Accrual, statutory/non-statutory split, and carry-over rules: [Holiday and Leave](holiday-and-leave.md).)
- **Holiday Allowance:** a holiday allowance of **8%** of your gross annual salary, paid out monthly on a pro-rata basis.
- **Travel Allowance:** **EUR 0.23 per kilometer** for commuting expenses. (Full travel/expense rules: [Spending and Contracting](spending-and-contracting.md).)
- **Professional Development:** the opportunity to dedicate **40 hours** annually to training and conferences; additional time is possible in consultation. (Process: [Learning & Personal Development](learning-personal-development.md).)
- **Flexible Public Holidays:** **flexible holidays** instead of the standard official Dutch public holidays, allowing you to choose the days that matter most to you. (Interchangeable Holiday Policy detail: [Holiday and Leave](holiday-and-leave.md).)
- **Conferences:** you are encouraged to visit (international) conferences to stay inspired and up to date.
- **Pension Scheme:** we invest in your future together. Respellion pays **50%** of the available premium, and you contribute the remaining **50%**. After joining, a meeting with our insurance advisor will be scheduled to explain the details of the scheme. (Full detail below.)
- **Disability Insurance (WIA):** Respellion has taken out WIA insurance for all employees. This insurance limits the drop in income in the event of long-term disability (longer than 2 years). The costs for this insurance are fully covered by Respellion.
- **Mobile Phone Allowance:** a monthly allowance of **EUR 35** for the use and potential purchase of a mobile phone.
## Your Pension (Detail)
At Respellion, we believe it is important to take good care of each other — not just today, but also with an eye toward the future. That is why we have established a pension scheme with **a.s.r. Doen Pensioen**, supported by advice from **Zuyderleven**.
### 1. The Basis: Defined Contribution Scheme
We use a Defined Contribution scheme (beschikbare premieregeling). This means that a fixed amount is invested every month for your future pension.
- **Pension Provider:** a.s.r. Doen Pensioen.
- **Contribution (Premium):** 10% of your pensionable salary (pension base) is contributed.
- **Employee Contribution:** based on our calculation models, we apply a split where you as the employee contribute 50% of this premium and Respellion contributes the remaining 50%.
### 2. How is the contribution calculated?
You do not build up a pension over your entire salary, because you will also receive AOW (state pension) from the government later in life. The portion of your salary over which you do build up a pension is called the pension base (pensioengrondslag).
- **Franchise (Threshold):** the part of your salary over which you do not build up a pension. For 2026, this is set at **EUR 19,172.00**.
- **Maximum Salary:** pension is built up on salary up to a maximum of **EUR 137,800.00**.
- **Calculation Example:** is your salary EUR 69,172? Then, with the 2026 franchise being EUR 19,172, your pension base will be EUR 50,000 (EUR 69,172 minus EUR 19,172), and your annual contribution (10%) will be EUR 5,000.
### 3. Investing via Life Cycles
Your pension funds are invested. Since your retirement date may still be far off, the risk is automatically reduced as you get older. We call this a Life Cycle.
- **Standard:** you usually start in a specific profile (e.g., Neutral), but you have the freedom to choose between different risk profiles: Defensive, Neutral, or Offensive.
- **Phasing out:** as you get closer to your retirement date, investments are shifted to safer options to minimize volatility and protect your capital.
### 4. Provisions for Death and Illness
In addition to saving for later, insurance is built in for unforeseen situations.
- **Partner's Pension:** in the event of your death, your partner will receive a benefit of 30% of your salary (for life). Note: this is on a risk basis, meaning coverage lapses if you leave Respellion's employment.
- **Orphan's Pension:** children receive 20% of your salary until they reach the age of 25.
- **ANW Gap Insurance:** you can voluntarily (at your own expense) take out extra insurance to provide a temporary supplement for your partner after death.
- **Disability:** in the event of disability (AO), your pension accrual continues (partially or fully) without you paying premiums (waiver of premium).
### 5. Flexibility and Insight
You have direct influence over your own pension:
- **Extra Savings:** if you want to build up more pension, you may voluntarily make additional contributions up to the fiscal maximum.
- **Portal:** through the 'Doen Pensioen' dashboard, you have 24/7 insight into your current accrual, returns, and the ability to submit your preferences.
## Related Topics
- [Holiday and Leave](holiday-and-leave.md) — vacation day accrual and carry-over, the Interchangeable Holiday Policy, and how unpaid leave affects pension and allowances.
- [Spending and Contracting](spending-and-contracting.md) — the EUR 0.23/km travel allowance and other expense rules.
- [Learning & Personal Development](learning-personal-development.md) — the 40 hours of professional development / conferences.
- [Day to Day](day-to-day.md) — travel-time compensation, distinct from the travel allowance.

View File

@@ -1,47 +1,90 @@
# Respellion — Roles & Accountabilities
Generated from `src/db/seed.ts` (source: GlassFrog export *Respellion Governance - 2026-04-26.pdf*).
Roles marked **(structural)** are constitutionally-required (Circle Lead, Facilitator, Secretary, Circle Rep).
--- ---
title: Roles & Accountabilities
category: Who we are
source_files:
- circlemanagement/ROLES.md # generated from GlassFrog governance export
summary: >
The Holacracy role structure of Respellion: the three circles (Anchor,
Operations, Software Delivery), the shared definitions of the structural roles
(Circle Lead, Facilitator, Secretary, Circle Rep), and the purpose and
accountabilities of every operational role. Personal names of role fillers have
been removed.
tags:
- roles
- accountabilities
- holacracy
- circles
- governance
- glassfrog
- structural-roles
related_topics:
- who-we-are.md # Holacracy model, QMS, services
- how-we-work-together.md # core values, coaching culture, huddles
- security.md # Privacy Officer ties to GDPR handling
- holiday-and-leave.md # People Officer administers leave
- learning-personal-development.md # People Officer maintains performance model
key_facts:
circles: [Respellion Anchor Circle, Respellion Operations, Software Delivery]
structural_roles: [Circle Lead, Facilitator, Secretary, Circle Rep]
anchor_purpose: "Een maatschappij waarin open source technologie wordt ingezet om duurzame oplossingen te bouwen."
huddle_count: 3 (A, B, C) — identical role definition, buddy structure
ai_context: >
This is the authoritative map of WHO-DOES-WHAT (by role, not by person) at
Respellion, and complements the abstract Holacracy model in who-we-are.md.
Personal filler names from the GlassFrog export have been intentionally removed;
do not infer or reintroduce individual names. Structural roles (Circle Lead,
Facilitator, Secretary, Circle Rep) are constitutionally required and share the
same definition across circles, defined once below; per-circle deviations are
noted explicitly. Many operational roles are referenced elsewhere in the
handbook: People Officer (leave, onboarding, performance model), Privacy Officer
(GDPR), Process Guardian (QMS/ISO 9001), Event Owner (conferences), Treasury
Keeper (pension/salary admin).
---
# Roles & Accountabilities
This is the role structure of Respellion under Holacracy. Roles — not people — hold purposes and accountabilities. The organization has three nested circles: the **Respellion Anchor Circle**, its sub-circle **Respellion Operations**, and that circle's sub-circle **Software Delivery**.
> **Note:** Personal names of role fillers have been removed from this knowledge-base version. Current role assignments live in GlassFrog.
## Structural Roles (shared definitions)
These four roles are constitutionally required. Unless a circle notes a deviation below, each instance uses the definition here.
**Circle Lead.** Holds the Purpose of the overall Circle. *(Deviation — Software Delivery Circle Lead adds accountabilities: making sure the roles and their purposes and accountabilities in the circle are described properly; assigning people to roles; monitoring fit, offering feedback to enhance fit, and re-assigning roles to others when useful; defining priorities and strategies for the circle.)*
**Facilitator.** Purpose: Circle governance and operational practices aligned with the Constitution. Accountabilities:
- Facilitating the Circle's regular Tactical Meetings
- Facilitating the Circle's Governance Process
- Triggering new elections for the Circle's elected Roles after each election term expires
- Auditing a Sub-Circle's meetings and records on request and declaring a Process Breakdown if one is discovered
**Secretary.** Purpose: Stabilize the Circle's constitutionally-required records and meetings. Domain: all governance records of the Circle. Accountabilities:
- Scheduling regular Tactical Meetings for the Circle
- Capturing and publishing Tactical Meeting outputs
- Scheduling Governance Meetings for the Circle
- Capturing and publishing the outputs of the Circle's Governance Process
- Interpreting the Constitution and anything under its authority upon request
**Circle Rep.** Purpose: Tensions relevant to process in a broader Circle channeled out and resolved. Accountabilities:
- Seeking to understand Tensions conveyed by Role Leads within the Circle
- Discerning Tensions appropriate to process within a broader Circle that holds the Circle
- Processing Tensions within a broader Circle to remove constraints on the Circle
## Respellion Anchor Circle ## Respellion Anchor Circle
**Purpose:** Een maatschappij waarin open source technologie wordt ingezet om duurzame oplossingen te bouwen. **Purpose:** Een maatschappij waarin open source technologie wordt ingezet om duurzame oplossingen te bouwen.
**Structural roles present:** Circle Lead, Facilitator, Secretary (all per shared definitions above).
### Aandeelhouders ### Aandeelhouders
- **Filler:** Patrick Smulders *(Shareholders — role present; filler name removed.)*
### Circle Lead *(structural)*
- **Purpose:** Een maatschappij waarin open source technologie wordt ingezet om duurzame oplossingen te bouwen.
### Facilitator *(structural)*
- **Purpose:** Circle governance and operational practices aligned with the Constitution.
- **Accountabilities:**
- Facilitating the Circle's regular Tactical Meetings
- Facilitating the Circle's Governance Process
- Triggering new elections for the Circle's elected Roles after each election term expires
- Auditing a Sub-Circle's meetings and records on request and declaring a Process Breakdown if one is discovered
### Secretary *(structural)*
- **Purpose:** Stabilize the Circle's constitutionally-required records and meetings.
- **Domains:** All governance records of the Circle
- **Accountabilities:**
- Scheduling regular Tactical Meetings for the Circle
- Capturing and publishing Tactical Meeting outputs
- Scheduling Governance Meetings for the Circle
- Capturing and publishing the outputs of the Circle's Governance Process
- Interpreting the Constitution and anything under its authority upon request
---
## Respellion Operations *(sub-circle of Anchor)* ## Respellion Operations *(sub-circle of Anchor)*
**Structural roles present:** Circle Lead, Circle Rep, Facilitator, Secretary (all per shared definitions above).
### Buddy ### Buddy
- **Purpose:** Onboard a new Respellion employee so this person feels welcome and at home. - **Purpose:** Onboard a new Respellion employee so this person feels welcome and at home.
@@ -52,7 +95,6 @@ Roles marked **(structural)** are constitutionally-required (Circle Lead, Facili
### Chief Azure ### Chief Azure
- **Purpose:** Ensure a maintainable and available Azure environment for Respellion. - **Purpose:** Ensure a maintainable and available Azure environment for Respellion.
- **Filler:** Robert van Diest
- **Accountabilities:** - **Accountabilities:**
- Monitoring availability of the Respellion Azure environment - Monitoring availability of the Respellion Azure environment
- Communicating with CSP (ALSO) - Communicating with CSP (ALSO)
@@ -61,19 +103,6 @@ Roles marked **(structural)** are constitutionally-required (Circle Lead, Facili
- Coordinating changes in resources above SLA with service manager - Coordinating changes in resources above SLA with service manager
- Communicating to Respellion stakeholders about the Azure environment - Communicating to Respellion stakeholders about the Azure environment
### Circle Lead *(structural)*
- **Purpose:** The Circle Lead holds the Purpose of the overall Circle.
- **Filler:** Patrick Smulders
### Circle Rep *(structural)*
- **Purpose:** Tensions relevant to process in a broader Circle channeled out and resolved.
- **Accountabilities:**
- Seeking to understand Tensions conveyed by Role Leads within the Circle
- Discerning Tensions appropriate to process within a broader Circle that holds the Circle
- Processing Tensions within a broader Circle to remove constraints on the Circle
### Event owner ### Event owner
- **Purpose:** Making sure Respellion colleagues have an awesome event experience. - **Purpose:** Making sure Respellion colleagues have an awesome event experience.
@@ -81,51 +110,23 @@ Roles marked **(structural)** are constitutionally-required (Circle Lead, Facili
- Drafting list of congresses - Drafting list of congresses
- Making sure there is a budget and using that budget - Making sure there is a budget and using that budget
- Making sure everything about a congress visit is facilitated (stay, transport, tickets, etc) - Making sure everything about a congress visit is facilitated (stay, transport, tickets, etc)
- Being transparant about the budget and the use of it - Being transparent about the budget and the use of it
- Taking ownership for small team events - Taking ownership for small team events
### Facilitator *(structural)* ### Huddle (A, B, C)
- **Purpose:** Circle governance and operational practices aligned with the Constitution. There are three Huddles (A, B, and C) that share an identical role definition; each acts as a buddy structure of three or four individuals.
- **Purpose:** A group of three or four individuals that acts as a buddy structure.
- **Accountabilities:** - **Accountabilities:**
- Facilitating the Circle's regular Tactical Meetings - Holding bi-weekly huddle check-ins
- Facilitating the Circle's Governance Process
- Triggering new elections for the Circle's elected Roles after each election term expires
- Auditing a Sub-Circle's meetings and records on request and declaring a Process Breakdown if one is discovered
### Huddle A
- **Purpose:** A group of three or four individuals that acts as buddy structure.
- **Fillers:** Lisanne Farla, Jos van Aalderen, Raymond Verhoef
- **Accountabilities:**
- Holding BI weekly huddle checkins
- Coaching each other on professional and personal issues - Coaching each other on professional and personal issues
- Strengthening the feedback culture with developing personal plans - Strengthening the feedback culture by developing personal plans
- Creating transparancy in individual progress and improvements - Creating transparency in individual progress and improvements
### Huddle B
- **Purpose:** A group of three or four individuals that acts as buddy structure.
- **Fillers:** Patrick Smulders, Edwin Hensen, Thomas Gunadi, Robert van Diest
- **Accountabilities:**
- Holding BI weekly huddle checkins
- Coaching each other on professional and personal issues
- Strengthening the feedback culture with developing personal plans
- Creating transparancy in individual progress and improvements
### Huddle C
- **Purpose:** A group of three or four individuals that acts as buddy structure.
- **Filler:** Edwin van den Houdt
- **Accountabilities:**
- Holding BI weekly huddle checkins
- Coaching each other on professional and personal issues
- Strengthening the feedback culture with developing personal plans
- Creating transparancy in individual progress and improvements
### Internal Auditor ### Internal Auditor
- **Purpose:** Making sure that independent evaluations of risks, processes, and controls within Respellion are conducted regularly. The goal is to ensure effective and efficient operations and to ensure compliance with laws and regulations. - **Purpose:** Making sure that independent evaluations of risks, processes, and controls within Respellion are conducted regularly. The goal is to ensure effective and efficient operations and compliance with laws and regulations.
- **Accountabilities:** - **Accountabilities:**
- Identifying and assessing risks within Respellion - Identifying and assessing risks within Respellion
- Analyzing the impact of these risks on operational and financial processes - Analyzing the impact of these risks on operational and financial processes
@@ -143,18 +144,16 @@ Roles marked **(structural)** are constitutionally-required (Circle Lead, Facili
### Marketing ### Marketing
- **Purpose:** Respellion has an outstanding corporate reputation that alligns with the core values and manifest. - **Purpose:** Respellion has an outstanding corporate reputation that aligns with the core values and manifest.
- **Filler:** Raymond Verhoef
- **Accountabilities:** - **Accountabilities:**
- Maintaining our exposure on social media - Maintaining our exposure on social media
- Maintaining the website on functional level - Maintaining the website on a functional level
- Developing relevant exposure on the website - Developing relevant exposure on the website
- Improving the corporate reputation in a structured and managed way - Improving the corporate reputation in a structured and managed way
### Networker ### Networker
- **Purpose:** Maintain a valuable and effective network of stakeholders. - **Purpose:** Maintain a valuable and effective network of stakeholders.
- **Filler:** Patrick Smulders
- **Accountabilities:** - **Accountabilities:**
- Maintaining a network of partners (ICT brokers, IT companies) - Maintaining a network of partners (ICT brokers, IT companies)
- Maintaining a network of potential customers - Maintaining a network of potential customers
@@ -165,9 +164,8 @@ Roles marked **(structural)** are constitutionally-required (Circle Lead, Facili
### People Officer ### People Officer
- **Purpose:** Making sure Respellion has/maintains the organizational culture in which individuals can be the best versions of themselves. And making sure Respellion has/maintains an unique and attractive organizational structure. - **Purpose:** Making sure Respellion has/maintains the organizational culture in which individuals can be the best versions of themselves, and making sure Respellion has/maintains a unique and attractive organizational structure.
- **Filler:** Raymond Verhoef - **Domain:** An asset, process, or other thing this Role may exclusively control and regulate as its property, for its purpose.
- **Domains:** An asset, process, or other thing this Role may exclusively control and regulate as its property, for its purpose.
- **Accountabilities:** - **Accountabilities:**
- Maintaining the performance model (salary etc) - Maintaining the performance model (salary etc)
- Facilitating the monthly celebration meeting - Facilitating the monthly celebration meeting
@@ -185,7 +183,6 @@ Roles marked **(structural)** are constitutionally-required (Circle Lead, Facili
### Privacy Officer ### Privacy Officer
- **Purpose:** To ensure Respellion maintains the highest standards of data privacy compliance while enabling the organization to effectively use data in alignment with our values of trust, courage, self-discipline, and entrepreneurship. - **Purpose:** To ensure Respellion maintains the highest standards of data privacy compliance while enabling the organization to effectively use data in alignment with our values of trust, courage, self-discipline, and entrepreneurship.
- **Filler:** Jos van Aalderen
- **Accountabilities:** - **Accountabilities:**
- Developing, implementing, and maintaining a comprehensive privacy program aligned with GDPR, Dutch privacy laws, and other applicable regulations - Developing, implementing, and maintaining a comprehensive privacy program aligned with GDPR, Dutch privacy laws, and other applicable regulations
- Monitoring and interpreting changes in privacy legislation and updating organizational policies accordingly - Monitoring and interpreting changes in privacy legislation and updating organizational policies accordingly
@@ -201,20 +198,18 @@ Roles marked **(structural)** are constitutionally-required (Circle Lead, Facili
### Process guardian ### Process guardian
- **Purpose:** Making sure Respellions organizational processes are transparant, compliant and scalable. - **Purpose:** Making sure Respellion's organizational processes are transparent, compliant and scalable.
- **Filler:** Raymond Verhoef
- **Accountabilities:** - **Accountabilities:**
- Maintaining a file structure for documents that facilitates compliance and transparancy - Maintaining a file structure for documents that facilitates compliance and transparency
- Maintaining a comprehensive quality management system that complies with potentional ISO regulations - Maintaining a comprehensive quality management system that complies with potential ISO regulations
- Advising the circle about effective software platforms - Advising the circle about effective software platforms
- Advising the circle about (auditing) the quality management system(s) - Advising the circle about (auditing) the quality management system(s)
- Advising the circle about any other relevant internal processes - Advising the circle about any other relevant internal processes
- Organizing internal audits as described in ISO9001:2015 chapter 9 - Organizing internal audits as described in ISO 9001:2015 chapter 9
### Recruiter ### Recruiter
- **Purpose:** Recruiting new employees who fit the Respellion's culture. - **Purpose:** Recruiting new employees who fit Respellion's culture.
- **Fillers:** Jos van Aalderen, Patrick Smulders
- **Accountabilities:** - **Accountabilities:**
- Selecting potential employees via necessary channels - Selecting potential employees via necessary channels
- Running the first telephone conversations in the application process - Running the first telephone conversations in the application process
@@ -228,40 +223,25 @@ Roles marked **(structural)** are constitutionally-required (Circle Lead, Facili
- Drafting job descriptions to attract potential employees - Drafting job descriptions to attract potential employees
- Reporting regularly on the recruiting process - Reporting regularly on the recruiting process
### Secretary *(structural)*
- **Purpose:** Stabilize the Circle's constitutionally-required records and meetings.
- **Filler:** Edwin van den Houdt
- **Domains:** All governance records of the Circle
- **Accountabilities:**
- Scheduling regular Tactical Meetings for the Circle
- Capturing and publishing Tactical Meeting outputs
- Scheduling Governance Meetings for the Circle
- Capturing and publishing the outputs of the Circle's Governance Process
- Interpreting the Constitution and anything under its authority upon request
### Service Manager ### Service Manager
- **Purpose:** Take ownership of the services we provide for customers with a SLA for hosting, maintenance and operations with the goal to be an excellent service provider. - **Purpose:** Take ownership of the services we provide for customers with an SLA for hosting, maintenance and operations, with the goal to be an excellent service provider.
- **Filler:** Raymond Verhoef
- **Accountabilities:** - **Accountabilities:**
- Defining, aligning and agree upon SLA's. - Defining, aligning and agreeing upon SLAs
- Managing ITIL service management processes. - Managing ITIL service management processes
- Acting as a single point of contact for hosting, operation and maintaining applications for customers. - Acting as a single point of contact for hosting, operation and maintaining applications for customers
### Sustainability officer ### Sustainability officer
- **Purpose:** Ensuring the balance between people-profit-planet in the Respellion corporate activities. - **Purpose:** Ensuring the balance between people-profit-planet in Respellion's corporate activities.
- **Filler:** Raymond Verhoef
- **Accountabilities:** - **Accountabilities:**
- Taking initiatives for social return activities - Taking initiatives for social return activities
- Challenging policy, activities and ideas against the corporate values and manifest and suggest changes/improvements - Challenging policy, activities and ideas against the corporate values and manifest and suggesting changes/improvements
- Communicating regularly to Respellion stakeholders about sustainability and social return activities - Communicating regularly to Respellion stakeholders about sustainability and social return activities
### System administrator ### System administrator
- **Purpose:** To maintain the Respellion company website and/or Respellion applications. - **Purpose:** To maintain the Respellion company website and/or Respellion applications.
- **Filler:** Thomas Gunadi
- **Accountabilities:** - **Accountabilities:**
- Monitoring the availability of the website - Monitoring the availability of the website
- Coordinating activities with hosting partner (GRRR) - Coordinating activities with hosting partner (GRRR)
@@ -271,8 +251,7 @@ Roles marked **(structural)** are constitutionally-required (Circle Lead, Facili
### Technology Officer ### Technology Officer
- **Purpose:** To create a technical strategy that aligns and add value to our purpose. - **Purpose:** To create a technical strategy that aligns with and adds value to our purpose.
- **Filler:** Edwin van den Houdt
- **Accountabilities:** - **Accountabilities:**
- Designing a strategy on open-source technology for Respellion and keeping it updated - Designing a strategy on open-source technology for Respellion and keeping it updated
- Defining internal technology projects - Defining internal technology projects
@@ -282,47 +261,39 @@ Roles marked **(structural)** are constitutionally-required (Circle Lead, Facili
### Treasury Keeper ### Treasury Keeper
- **Purpose:** Respellion has a sustainable and transparent financial administration that meets legal obligations. - **Purpose:** Respellion has a sustainable and transparent financial administration that meets legal obligations.
- **Filler:** Patrick Smulders
- **Accountabilities:** - **Accountabilities:**
- Maintaining salary administration - Maintaining salary administration
- Maintaining declaration administration - Maintaining declaration administration
- Maintaining company budget sheets - Maintaining company budget sheets
- Maintaining pension administration - Maintaining pension administration
- Maintaining insurance administration - Maintaining insurance administration
- Reporting on the administrations to create transparency. - Reporting on the administrations to create transparency
- Communicating to Respellion stakeholders about the financial adminstration - Communicating to Respellion stakeholders about the financial administration
### Workplace support ### Workplace support
- **Purpose:** Ensure a safe and effective digital workplace. - **Purpose:** Ensure a safe and effective digital workplace.
- **Filler:** Thomas Gunadi
- **Accountabilities:** - **Accountabilities:**
- Ensuring security compliancy conform Microsoft security policies - Ensuring security compliance conform Microsoft security policies
- Facilitating healthy workplace - Facilitating a healthy workplace
- Supporting colleagues in IT (with regard to hardware and software) - Supporting colleagues in IT (with regard to hardware and software)
- Maintaining Microsoft 365 - Maintaining Microsoft 365
- Providing software licenses to colleagues if needed - Providing software licenses to colleagues if needed
---
## Software Delivery *(sub-circle of Respellion Operations)* ## Software Delivery *(sub-circle of Respellion Operations)*
**Purpose:** Deliver high quality software and have happy customers. **Purpose:** Deliver high quality software and have happy customers.
### Circle Lead *(structural)* **Structural roles present:**
- **Purpose:** The Circle Lead holds the Purpose of the overall Circle. - **Circle Lead** — uses the expanded accountabilities noted in the shared Circle Lead definition above (describing roles, assigning people, monitoring fit, defining priorities/strategies).
- **Filler:** Thomas Gunadi - **Facilitator** — no purpose, accountabilities, or domains defined.
- **Accountabilities:** - **Secretary** — no purpose, accountabilities, or domains defined.
- Making sure the roles and their purposes and accountabilities in the Software Delivery Circle are described properly
- Assigning people to roles
- Monitoring the fit, offering feedback to enhance fit, and re-assigning roles to others when useful for enhancing fit
- Defining priorities and strategies for the circle
### Facilitator *(structural)* ## Related Topics
*No purpose, accountabilities, or domains defined.* - [Who We Are](who-we-are.md) — the Holacracy model, circles, tensions, and ISO 9001 QMS these roles operate within.
- [How We Work Together](how-we-work-together.md) — core values referenced in role purposes, and the coaching culture the Huddles enact.
### Secretary *(structural)* - [Security](security.md) — GDPR handling that the Privacy Officer role owns.
- [Holiday and Leave](holiday-and-leave.md) — leave/sickness administration owned by the People Officer.
*No purpose, accountabilities, or domains defined.* - [Learning & Personal Development](learning-personal-development.md) — the performance model maintained by the People Officer and conferences owned by the Event Owner.

View File

@@ -0,0 +1,89 @@
---
title: Security
category: Security
source_files:
- docs/Security/working-with-personal-data.md
- docs/Security/bhv-evacuation.md
summary: >
Working with personal data under the GDPR (what counts as processing and as
personal data, ordinary vs special vs sensitive categories, and the BSN rules),
plus a placeholder for BHV/evacuation and RI&E procedures.
tags:
- gdpr
- avg
- personal-data
- privacy
- bsn
- data-classification
- bhv
- evacuation
- safety
related_topics:
- who-we-are.md # ISO 9001 QMS, "security by design" quality policy
- spending-and-contracting.md # 1Password / shared accounts, secure storage
status_notes:
bhv_evacuation: PLACEHOLDER — content not yet written in source.
ai_context: >
"GDPR" and the Dutch "AVG" are the same regulation. "BSN" = Burgerservicenummer
= Dutch Citizen Service Number / national identification number. The QMS quality
policy in who-we-are.md commits to "security by design", which this section
operationalizes for personal-data handling. The BHV/evacuation subsection is an
unfilled placeholder in the source material and should not be treated as
authoritative guidance.
---
# Security
This section covers data protection (GDPR) and workplace safety (BHV/evacuation, RI&E).
## Working with Personal Data: Understanding the GDPR
In providing our services, we frequently work with personal data. Under the General Data Protection Regulation (GDPR), this is referred to as "processing." But what does this actually mean? And what exactly constitutes personal data?
The GDPR came into effect on **May 25, 2018**. This is a European regulation that governs the processing of personal data. The GDPR has significant implications for our organization because we constantly work with the personal data of individuals.
### When does the GDPR apply?
The GDPR applies to the processing of personal data. The GDPR does **not** apply to the data of deceased individuals.
First, you must determine if the actions you perform with data constitute "processing." According to the GDPR, processing is any operation or set of operations performed on personal data. Common operations include: collecting, recording, storing, altering, retrieving, consulting, using, disclosing, erasing, and destroying data.
In practice, this means that almost any handling of personal data is considered "processing" under the GDPR.
### What is personal data?
The GDPR defines personal data as any information relating to an identified or identifiable natural person. This means information that is directly about an individual or can be traced back to that individual. Data about organizations is **not** considered personal data under the GDPR.
### Examples of Personal Data
There are many types of personal data. Obvious examples include a person's name, address, and place of residence. However, telephone numbers and postal codes combined with house numbers are also personal data.
Indirect personal data can also exist. This refers to data that, when combined with other information, reveals something about an individual or can be traced back to them.
**Ordinary Personal Data.** Examples include names, file numbers, or contact details. Ensure that access to or viewing of this data is restricted only to individuals for whom it is necessary for their role.
**Special Categories of Personal Data.** Sensitive data such as a person's race, religion, health, criminal record, and biometric data (e.g., fingerprints) are referred to as special categories of personal data. Processing special categories of personal data is prohibited unless a specific legal exception applies. Consult your designated privacy officer if you use or intend to use this type of data.
**Sensitive Personal Data.** Some data may not be classified as "special category" by definition but is sensitive enough to warrant extra precautions. This includes data concerning:
- Electronic communications
- Location data
- Financial data (such as income or purchasing behavior)
- Citizen Service Numbers (BSN) / National Identification Numbers
Genetic personal data is also sensitive because it provides unique information about an individual's physiology or health, and/or the health of their family members, making the information particularly delicate. For instance, Citizen Service Numbers (BSN) may only be used for purposes prescribed by law. It is not permitted to process or use these numbers for other purposes. Exercise extreme caution when handling such data. Ensure that this data is stored in secure applications and **not** in unsecured files on local or shared drives.
### BSN: Citizen Service Number (Burgerservicenummer)
A national identification number is a unique number established by law. In the Netherlands, the most well-known national identification number is the Citizen Service Number (BSN). These numbers may only be used for purposes prescribed by law. Processing them for purposes other than those legally specified is not permitted.
Examples of Dutch laws regulating the use of the BSN include the General Provisions for Citizen Service Number Act (Wet algemene bepalingen burgerservicenummer), the Use of Citizen Service Number in Healthcare Act (Wet gebruik burgerservicenummer in de zorg), and the Personal Identification Numbers in Education Act (Wet persoonsgebonden nummers in het onderwijs).
## BHV & Evacuation Plan, RI&E
> **Placeholder.** Evacuation and safety procedures (BHV, evacuation plan, and the Risk Inventory & Evaluation / RI&E) are not yet documented in the source material. This subsection should be completed with the actual procedures before being relied upon.
## Related Topics
- [Who We Are](who-we-are.md) — the ISO 9001 QMS and its "security by design" quality policy, which this data-protection guidance puts into practice.
- [Spending and Contracting](spending-and-contracting.md) — use of 1Password and shared accounts, relevant to storing sensitive data securely.

View File

@@ -0,0 +1,113 @@
---
title: Spending and Contracting
category: Spending and contracting
source_files:
- docs/Spending and contracting/spending-purchasing.md
summary: >
The spending and purchasing policy: principles, basic rules, expense categories
(travel, professional development, equipment, team events/food & drinks), and
the submission process via NMBRS — within Dutch WKR tax rules.
tags:
- spending
- purchasing
- expenses
- reimbursement
- nmbrs
- wkr
- travel-allowance
- professional-development
- equipment
related_topics:
- day-to-day.md # travel-time policy (time vs km allowance)
- learning-personal-development.md # conferences/courses being expensed
- pension-scheme-and-benefits.md # allowances as benefits
- security.md # 1Password for shared subscription accounts
key_facts:
travel_km_allowance: EUR 0.23 per kilometer (commuting and business travel)
expense_submission_deadline_days: 30
expense_tool: NMBRS
laptop_bag_max: EUR 100
finance_contact: finance@respellion.com
ai_context: >
"WKR" = Werkkostenregeling, the Dutch work-related costs tax scheme that
constrains what can be reimbursed (notably why personal food/drinks are not
reimbursable and team events should relate to customer activities). The
EUR 0.23/km here is the same travel allowance summarized in the benefits
overview (pension-scheme-and-benefits.md); it is distinct from travel-TIME
compensation in day-to-day.md. Expenses go through NMBRS; structured spending
requests (subscriptions, software, conferences, peripherals) go through a
spending form on SharePoint.
---
# Spending and Contracting
## Spending and Purchasing
### General Principles
The goal of our spending and purchasing policy is to enable roles to be fully accountable for the work they do, including spending.
We trust our team members to make sensible decisions about company expenses. This policy provides guidelines to ensure clarity and compliance with Dutch tax regulations. For our employees, we have more details available about the Dutch tax regulations and the werkkostenregeling (WKR) in our Werkkostenregeling (WKR) document.
### Basic Rules
1. Spend company money as if it were your own.
2. Recurring items are monitored.
3. Submit expenses within 30 days.
4. Always upload clear photos of original receipts in [NMBRS](https://www.nmbrs.com/nl/inloggen).
5. Choose the right "Type/Soort" and the right "Description/Beschrijving".
### Request for asset expenses
For some of the assets we have in our company, we want a registration to ensure validity and to prevent unnecessary subscriptions that lead to high costs. The administration for these assets is available: Books and Subscriptions.
### Expense Categories
The following expenses are suitable for expense declarations. The information within brackets is necessary in [NMBRS](https://www.nmbrs.com/nl/inloggen) for correct administration of the costs.
**Travel**
- Public transport: fully reimbursed for business travel via NS business card.
- Car, motorcycle, bicycle: **EUR 0.23 per kilometer** for commuting and/or business travel. [Include address of the commute departure and arrival per date.]
- Parking costs for customer business meeting. [Description: Klantnaam]
**Professional Development**
- Books: you can buy books that are useful for your professional development. These books remain the property of Respellion. Physical books are stored at the office and digital books are available via the Respellion library. Please check if a book or subscription has already been purchased — we keep an administration in SharePoint. [Description: Zakelijke literatuur]
- Digital platforms with a subscription can be requested via the spending form on SharePoint. When approved, you can purchase the subscription and it will be registered in SharePoint. If possible, use a general account (through 1Password) to share with colleagues. We will check this list with you during the year to see if subscriptions are still relevant. [Description: Zakelijk abonnement]
- Conferences, seminars and such can be requested via the spending form on SharePoint. [Description: Conferenties, cursussen]
- Software licenses can be requested via the spending form on SharePoint. [Description: no additional comment on receipt]
- Exam costs for work-related certifications can be requested via the spending form on SharePoint. [Description: no additional comment on receipt]
**Equipment**
- Laptop (company provided).
- Laptop bag if necessary, with a maximum of **EUR 100**. [Description: no additional comment]
- Peripherals (webcam, mouse, etc.) can be requested via the spending form on SharePoint. [Description: no additional comment]
**Team events, food & drinks**
- We want you to feel comfortable to organise informal gatherings with colleagues. You are allowed to spend a reasonable amount of money on food and drinks during these gatherings. It is important that these gatherings have a relation with customer activities if possible, because of WKR regulations. [Description: topic of the event and with customer name]
- Because of WKR regulations, it is not allowed to expense personal food and drinks.
- Personal food and drinks are not reimbursed. Coffee and tea is available in the office and we keep a stocked refrigerator (when empty, contact the workplace facilitator).
- Costs for food and drinks during non-regular customer meetings are reimbursed. [Description: topic of the event and with customer name]
- Costs for food and drinks as a result of overtime are reimbursed. [Description: Overwerk]
### Submission Process
1. Use our expense app ([NMBRS](https://www.nmbrs.com/nl/inloggen)).
2. Upload clear receipt photos.
3. Add a clear description as described in this policy.
4. Submit within 30 days.
5. Reimbursement with next salary payment.
### Questions?
Contact finance@respellion.com for any questions about this policy.
## Related Topics
- [Day to Day → Travel Time Policy](day-to-day.md) — travel-TIME compensation (1.5-hour threshold), distinct from the EUR 0.23/km allowance above.
- [Learning & Personal Development](learning-personal-development.md) — conference and course attendance that is requested/expensed here.
- [Pension Scheme & Benefits](pension-scheme-and-benefits.md) — travel, phone, and other allowances listed as benefits.
- [Security](security.md) — secure handling of shared subscription credentials via 1Password.

View File

@@ -0,0 +1,240 @@
---
title: Who We Are
category: Who we are
source_files:
- docs/Who we are/what-does-respellion.md
- docs/Who we are/respellion-model.md
- docs/Who we are/qms-design.md
- docs/Who we are/your-first-day.md
summary: >
Respellion's identity, services, self-organizing operating model (Holacracy),
ISO 9001 Quality Management System, and the onboarding ("first day") checklist.
tags:
- identity
- services
- holacracy
- governance
- quality-management
- iso-9001
- onboarding
related_topics:
- how-we-work-together.md # purpose, core values, circles/huddles, coaching culture
- learning-personal-development.md # coaching culture, growth & reward
- day-to-day.md # meeting cadence, time registration, task board
key_facts:
core_values: [Trust, Courage, Self-discipline, Entrepreneurship]
governance_model: Holacracy
operational_standard: Agile / DevOps
quality_standard: ISO 9001:2015
governance_tooling: Glassfrog
meeting_notes_tooling: Microsoft Loop
ai_context: >
This file defines the organizational foundation referenced everywhere else in
the handbook. "Roles" (not job titles), "Circles", "Tensions", "Tactical" and
"Governance" meetings, "Lead Link", and "Facilitator" are Holacracy terms used
throughout other documents. The core values (Trust, Courage, Self-discipline,
Entrepreneurship) are cited as the rationale for many concrete policies
(e.g. flexible hours, spending, coaching culture).
---
# Who We Are
This section covers what Respellion does, how the organization is structured (the Holacracy-based "Respellion model"), the ISO 9001 Quality Management System, and what a new colleague does on their first day.
## What Does Respellion Actually Do?
### We develop and maintain software
Respellion is an organization that has knowledge and experience about technology and organization. We have a clear vision of how digitization can best be technically developed and managed, but also how this can be organized in an efficient way. This combination is exceptionally powerful and leads to successful solutions.
Our services focus on result-driven projects, high performance teams, and consultancy.
### Projects
Based on years of experience with developing software in multiple organizations, we know how to organize this effectively in an Agile manner and how to execute it in a result-driven way.
Our methodology ensures that we stay in control of progress and budget, in both the development phase and maintenance phase. Our developers endorse the Agile Manifesto as a starting point for daily activities. This provides a basis for big and small decision-making. By using metrics, we know at any time where we stand and what is needed to achieve the ultimate goal.
We are able to scale up and down in a timely manner with the right expertise needed, such as developers, architects, testers, analysts, UX designers and DevOps engineers.
### High performance teams
We deliver high performance teams. In our vision, these are teams of IT professionals with in-depth technical knowledge, who bring the DNA of our organization. The core values of courage, self-discipline, responsibility, and entrepreneurship ensure a way of working together that creates synergy between individual competences.
Team members are highly trained T-shaped professionals who perform all the work necessary to deliver results. By attending training courses, conferences and workshops, they stay up-to-date with the latest developments in their field of expertise. In addition, our agile coaches stimulate new insights and awareness, so that there is attention to the wellbeing of the individual.
For us, high performance also means working closely with our customers and striving for continuous improvement. We continuously evaluate our performance, learn from successes and from failures, and adapt processes to get better and better.
### Consultancy
We have thought leaders who help organizations with strategic challenges and advice, who can perform technical analysis, develop tailor-made solutions, and support organizations in making IT decisions.
We specialize in IT architecture, software development, information security, quality assurance, agile management and coaching. Our services consist of interim deployment, but also of result-driven or individual assignments.
## The Respellion Model: No Management, No Job Descriptions
### Core Principles
Holacracy is a governance framework that enables self-organization without traditional management hierarchies. At Respellion, we've adopted this approach to align with our core values:
- Trust
- Courage
- Self-discipline
- Entrepreneurship
### Why Holacracy?
Our observations from the field show that while agile frameworks effectively facilitate self-organization in teams, organizational structures often remain rigid. Holacracy addresses this by:
- Transforming static job descriptions into dynamic roles
- Shifting from delegated authority to distributed authority
- Replacing large reorganizations with constant micro-iterations
- Substituting office politics with transparent rules and disciplined processes
### Key Elements
1. **Role-Based Structure:** Individuals fill one or more clearly defined roles within the organization, each with specific accountabilities.
2. **Circle Organization:** Related roles form circles that are connected through link roles.
3. **Dynamic Governance:** Roles are defined, amended, or removed through a formal governance process.
4. **Meeting Formats:** Structured meetings focused on action rather than over-analysis.
5. **Tension-Driven Evolution:** The organization evolves through addressing "tensions" — gaps between what is and what could be.
### Practical Implementation
Respellion operates with:
- One organizational circle
- Monthly tactical and governance meetings (Fridays)
- Huddle check-in meetings twice a week
- A Huddle Elevator session every two months
- A task board for tracking work
- Microsoft Loop for administration and meeting notes
### Meeting Structure
All meetings are administered in Microsoft Loop.
**Tactical Meetings**
- Focus on operational issues and coordinating next actions
- Result in clear tasks assigned to specific roles
- Tasks are tracked on the Respellion Task Board
**Governance Meetings**
- Focus on evolving the organizational structure
- Create, modify, or remove roles based on emerging needs
- Follow a structured process to address tensions through role changes
- Meeting notes are administered in Governance Meeting Notes
### The Concept of Tension
A tension represents an opportunity for improvement — a gap between current reality and potential reality. Tensions can be resolved by:
- Providing information
- Creating new roles
- Defining specific actions
This approach empowers our people with real responsibility, entrepreneurial freedom, and the ability to evolve our organization continuously.
**Reference:** Blog [Holacracy](https://www.holacracy.org/blog/)
## Quality Manual (QMS) — ISO 9001:2015
This describes the Quality Management System (QMS) of Respellion. Our QMS is designed to deliver maximum value to our customers through "sustainable digitization" without unnecessary bureaucracy. We use Holacracy as a governance model and Agile/DevOps as an operational standard.
### 4. Context of the Organization
**4.1 Understanding the Organization and Its Context** — Respellion operates at the intersection of open-source technology and societal impact. Our context is determined by the need for safe, scalable, and sustainable digital solutions. Documentation: Manifesto, Company Report. Review: Annually during a strategic governance session of the General Board Circle.
**4.2 Stakeholders and Their Needs** — We identify our stakeholders (customers, employees/roles, government, partners) through our circle structure. Implementation: Roles and their 'accountabilities' in Glassfrog define responsibilities towards stakeholders. Evidence: Glassfrog governance overview.
**4.3 Scope** — The QMS applies to: "The design, development, implementation, and maintenance of customized software solutions and the provision of consultancy in the field of digitization and open-source technology."
**4.4 Quality Management System and Processes** — Our processes are not linear but cyclical (Agile). The interaction between processes is managed through Holacracy meetings (Tactical & Governance).
### 5. Leadership
**5.1 Leadership and Commitment** — At Respellion, leadership is distributed. The roles of Lead Link (strategy/prioritization) and Facilitator (process oversight) ensure a focus on quality. Our core values (Trust, Courage, Self-discipline, Entrepreneurship) form the basis for quality awareness.
**5.2 Quality Policy** — Our policy is focused on "Sustainable Digitization." We commit to: meeting customer and legal requirements; continuous improvement of our 'rebellious' approach; security by design.
**5.3 Roles, Responsibilities, and Authorities** — In accordance with Holacracy, all roles, responsibilities, and authorities are transparently documented in Glassfrog. There are no hierarchical positions, only dynamic roles.
### 6. Planning
**6.1 Risks and Opportunities** — Risk management is an ongoing process for us, not an annual document. Trigger: a felt 'Tension' (gap between current and desired situation). Action holder: any role within the relevant Circle. Action: bring the Tension into a Tactical or Governance meeting. Output: output of the meeting (projects or governance changes).
**6.2 Quality Objectives** — Objectives are defined per Circle in the form of OKRs or Circle Purposes. Measurement: progress is discussed in the monthly Tactical Meetings.
### 7. Support
**7.1 and 7.2 Resources and Competencies** — People are our most important capital. A formal review of the development and contribution of role-fillers takes place twice a year. Huddle Elevator: coaching and feedback sessions ensure knowledge levels.
**7.3 Awareness** — Quality awareness is ensured through Onboarding and Huddle Check-ins.
**7.4 Communication** — Communication about the QMS is done during our monthly Tactical meeting. We also focus a lot on our quality policy and our quality goals during the Onboarding process and in our Employee Handbook.
**7.5 Documented Information** — We minimize static documents.
- Source codes: Git (version control is evidence).
- Processes: Glassfrog (Governance).
- Tasks: Task Board (Jira/Azure DevOps).
### 8. Execution (Operations)
**8.1 Operational Planning and Control** — We work according to the Agile methodology (Scrum/Kanban). Quality gate: the Definition of Done (DoD) serves as the primary quality control (ISO 9001:8.1). Each user story must meet the DoD (e.g., peer review, automated tests, security scan).
**8.2 Requirements for Products and Services** — Trigger: customer request or Request for Proposal. Action holder: role 'Contracting' or 'Product Owner'. Output: signed proposal or approved Backlog items.
**8.3 Design and Development** — Software development follows the CI/CD pipeline. Review: peer reviews (Pull Requests) are mandatory for every code change (ISO 9001:8.3.4). Validation: User Acceptance Testing (UAT) by the customer before release.
**8.4 Control of Externally Provided Services** — Suppliers (Cloud providers, SaaS tools) are selected based on security and sustainability criteria (ISO 14001 integration).
### 9. Performance Evaluation
**9.1 Monitoring and Measurement** — Customer satisfaction: measured directly during Sprint Demos and through informal feedback loops. Process performance: Velocity and Lead Time in Agile boards.
**9.2 Internal Audit** — We continuously audit ourselves by testing the 'Governance' against reality. Formal audit: annually, a designated role outside the circle conducts a check on compliance with its own process agreements in Glassfrog.
**9.3 Management Review** — In accordance with Holacracy, we replace the classic management review with a specific Strategic Review in the General Board Circle.
- Input: overview of Tensions, customer feedback, financial status, incidents.
- Output: strategic course changes and updates to the QMS.
### 10. Improvement
**10.1 General and 10.2 Deviations** — Mistakes are learning opportunities ("Most Rebellious Failures").
- Trigger: a bug, incident, or customer complaint.
- Action holder: the relevant Role within the Huddle.
- Action: root cause analysis during the Retrospective (ISO 9001:10.2).
- Output: improvement action on the Backlog or adjustment in the DoD.
**10.3 Continuous Improvement** — Our structure is self-cleaning. By continuously processing Tensions in Governance meetings, the system improves itself every month.
## Your First Day
Welcome to Respellion! We have collected some useful information for you to start with. In the first few days, we also plan a few appointments in your calendar.
You can start with the following activities:
- Check the onboarding checklist on SharePoint and filter on name
- View our SharePoint environments
- Read through the Employee Handbook
- If you work on internal activities, you turn them into an activity on the Respellion Taskboard
- Write a short profile text for the Respellion website (see examples)
- Create your personal plan on the Miro board
Here are a few topics for the first few weeks that we will discuss together:
- Introduction presentations:
- Respellion Introduction.pptx
- Respellion Holacracy
- Coaching Culture
- Check the current organizational model on [Glassfrog](https://app.glassfrog.com/organizations/32697/orgnav/roles/13886133)
All the necessary information is available via our employee handbook or the internal docs.
## Related Topics
- [How We Work Together](how-we-work-together.md) — purpose, the four core values explained, the manifest, circles/huddles, and the coaching culture that the QMS and Holacracy model reference.
- [Learning & Personal Development](learning-personal-development.md) — the coaching culture, Personal Development Plan (POP), and Growth & Reward model that operationalize the "people are our most important capital" QMS principle.
- [Day to Day](day-to-day.md) — time registration on the task board, meeting cadence, and the practical working rhythm implied by the Holacracy model.

View File

@@ -8,8 +8,13 @@ perspective — interaction patterns, completion rules, and the relationship
between micro learnings, topics, and sessions. between micro learnings, topics, and sessions.
This document is application-agnostic. It does not describe how micro This document is application-agnostic. It does not describe how micro
learnings are generated, stored, or administered. For those topics see the learnings are generated, stored, or administered. For those topics see
micro learning generation specification. `docs/generation-spec.md`.
> **Current types:** the platform ships **three** micro-learning types —
> `concept_explainer`, `scenario_quiz`, and `flashcard_set`. A fourth
> (`reflection_prompt`) was specced earlier and has since been dropped.
> References to it below are retained only as historical context.
--- ---
@@ -47,17 +52,15 @@ Theme (one per weekly session)
└── Topic A └── Topic A
│ ├── Concept explainer ← micro learning │ ├── Concept explainer ← micro learning
│ ├── Scenario quiz ← micro learning │ ├── Scenario quiz ← micro learning
── Flashcard set ← micro learning ── Flashcard set ← micro learning
│ └── Reflection prompt ← micro learning
└── Topic B └── Topic B
├── Concept explainer ├── Concept explainer
├── Scenario quiz ├── Scenario quiz
── Flashcard set ── Flashcard set
└── Reflection prompt
``` ```
One session covers one Theme. A Theme contains multiple Topics. Each Topic One session covers one Theme. A Theme contains multiple Topics. Each Topic
has up to four micro learnings — one per type. The employee selects which has up to three micro learnings — one per type. The employee selects which
type to engage with per Topic per session. type to engage with per Topic per session.
--- ---
@@ -97,7 +100,6 @@ Each type has its own completion trigger:
| Concept explainer | Employee reaches the end of the content | | Concept explainer | Employee reaches the end of the content |
| Scenario quiz | Employee selects an answer and views the explanation | | Scenario quiz | Employee selects an answer and views the explanation |
| Flashcard set | Employee views all cards in the set at least once | | Flashcard set | Employee views all cards in the set at least once |
| Reflection prompt | Employee submits a response (any response) |
Completion is not quality-gated. The employee does not need to answer Completion is not quality-gated. The employee does not need to answer
correctly or respond thoughtfully. The act of engaging with the full micro correctly or respond thoughtfully. The act of engaging with the full micro
@@ -128,7 +130,11 @@ per session.
--- ---
## The four types — learner perspective ## The micro-learning types — learner perspective
> Three types are active: **Concept explainer**, **Scenario quiz**, and
> **Flashcard set**. The **Reflection prompt** subsection below is retained for
> historical context only — that type is not currently generated or shown.
### Concept explainer ### Concept explainer
@@ -296,28 +302,33 @@ a completion and contributes to the employee's history.
These are logical entities described from the learner's perspective. These are logical entities described from the learner's perspective.
Implementation may map them to any storage system. Implementation may map them to any storage system.
**MicroLearning** (the artifact) These map to the `micro_learnings` and `micro_learning_completions` PocketBase
collections (see `docs/data-model.md`).
**MicroLearning** (the artifact — `micro_learnings`)
``` ```
id id
topic → Topic topic_id → Topic
type concept_explainer | scenario_quiz | type concept_explainer | scenario_quiz | flashcard_set
flashcard_set | reflection_prompt content structured data per type schema (JSON)
content structured data per type schema
status published (only published items are visible to employees) status published (only published items are visible to employees)
``` ```
**MicroLearningCompletion** (the event) **MicroLearningCompletion** (the event`micro_learning_completions`)
``` ```
id id
employee → Employee team_member_id → team member (employee)
micro_learning → MicroLearning micro_learning_id → MicroLearning
topic → Topic topic_id → Topic
type type identifier at time of completion type type identifier at time of completion
completed_at datetime session_week integer — the user's absolute curriculum week when completed
session_week integer — curriculum week when completed created datetime (PocketBase autodate)
cycle integer — which cycle
``` ```
`session_week` is the per-user absolute week counter (week 1 begins the day the
employee enrolls). The 26-week slot and cycle are derived from it; there is no
separate stored `cycle` field.
Completions are append-only. They are never updated or deleted. Each Completions are append-only. They are never updated or deleted. Each
represents a discrete learning event in the employee's history. represents a discrete learning event in the employee's history.
@@ -346,14 +357,12 @@ represents a discrete learning event in the employee's history.
3. An employee who selects an incorrect answer in a scenario quiz sees 3. An employee who selects an incorrect answer in a scenario quiz sees
explanations for all options, not only their chosen option explanations for all options, not only their chosen option
4. An employee cannot complete a flashcard set without viewing all cards 4. An employee cannot complete a flashcard set without viewing all cards
5. An employee who submits any text in a reflection prompt completes the 5. Completing one type for a Topic does not remove or replace the
micro learning — the content of the response is not evaluated
6. Completing one type for a Topic does not remove or replace the
completion record for another type for the same Topic completion record for another type for the same Topic
7. A Topic with one completed type counts as engaged for session progress 6. A Topic with one completed type counts as engaged for session progress
8. An employee can access and complete micro learnings from the knowledge 7. An employee can access and complete micro learnings from the knowledge
library outside of their scheduled session week library outside of their scheduled session week
9. Each completion creates exactly one record — submitting a reflection 8. Each completion creates exactly one record — completing the same type
prompt twice creates two records, not one updated record twice creates two records, not one updated record
10. An employee in cycle 2 can complete a flashcard set for a Topic they 9. An employee in cycle 2 can complete a flashcard set for a Topic they
completed in cycle 1 — the new completion is recorded independently completed in cycle 1 — the new completion is recorded independently

View File

@@ -1,181 +0,0 @@
# Respellion — Roles & Accountabilities
Generated from `src/db/seed.ts` (source: GlassFrog export *Respellion Governance - 2026-04-26.pdf*).
Roles marked **(structural)** are constitutionally-required (Circle Lead, Facilitator, Secretary, Circle Rep).
---
## Respellion Anchor Circle
**Purpose:** Een maatschappij waarin open source technologie wordt ingezet om duurzame oplossingen te bouwen.
### Circle Lead *(structural)*
- **Purpose:** Een maatschappij waarin open source technologie wordt ingezet om duurzame oplossingen te bouwen.
### Facilitator *(structural)*
- **Purpose:** Circle governance and operational practices aligned with the Constitution.
- **Accountabilities:**
- Facilitating the Circle's regular Tactical Meetings
- Facilitating the Circle's Governance Process
- Triggering new elections for the Circle's elected Roles after each election term expires
- Auditing a Sub-Circle's meetings and records on request and declaring a Process Breakdown if one is discovered
### Secretary *(structural)*
- **Purpose:** Stabilize the Circle's constitutionally-required records and meetings.
- **Domains:** All governance records of the Circle
- **Accountabilities:**
- Scheduling regular Tactical Meetings for the Circle
- Capturing and publishing Tactical Meeting outputs
- Scheduling Governance Meetings for the Circle
- Capturing and publishing the outputs of the Circle's Governance Process
- Interpreting the Constitution and anything under its authority upon request
---
## Respellion Operations *(sub-circle of Anchor)*
### Chief Azure
- **Purpose:** Ensure a maintainable and available Azure environment for Respellion.
- **Filler:** Robert van Diest
- **Accountabilities:**
- Monitoring availability of the Respellion Azure environment
- Communicating with CSP (ALSO)
- Maintaining the infrastructure for internal and customer purposes
- Following up any alerts from Azure
- Coordinating changes in resources above SLA with service manager
- Communicating to Respellion stakeholders about the Azure environment
### Circle Lead *(structural)*
- **Purpose:** The Circle Lead holds the Purpose of the overall Circle.
- **Filler:** Patrick Smulders
### Event owner
- **Purpose:** Making sure Respellion colleagues have an awesome event experience.
- **Accountabilities:**
- Drafting list of congresses
- Making sure there is a budget and using that budget
- Making sure everything about a congress visit is facilitated (stay, transport, tickets, etc)
- Being transparant about the budget and the use of it
- Taking ownership for small team events
### Facilitator *(structural)*
- **Purpose:** Circle governance and operational practices aligned with the Constitution.
- **Accountabilities:**
- Facilitating the Circle's regular Tactical Meetings
- Facilitating the Circle's Governance Process
- Triggering new elections for the Circle's elected Roles after each election term expires
- Auditing a Sub-Circle's meetings and records on request and declaring a Process Breakdown if one is discovered
### Internal Auditor
- **Purpose:** Making sure that independent evaluations of risks, processes, and controls within Respellion are conducted regularly. The goal is to ensure effective and efficient operations and to ensure compliance with laws and regulations.
- **Accountabilities:**
- Identifying and assessing risks within Respellion
- Analyzing the impact of these risks on operational and financial processes
- Developing an annual (internal) audit plan based on risk analysis, including scope and objectives
- Conducting operational, financial and compliance audits
- Collecting and analyzing data to assess effectiveness of internal controls
- Preparing audit reports (findings, conclusions, recommendations)
- Presenting results to management and colleagues
- Monitoring the implementation of recommendations
- Evaluating the effectiveness of corrective actions
- Ensuring compliance with internal guidelines and external law and regulations
- Advising on improvements to processes and controls to ensure compliance
- Contributing to the development of awareness of internal controls within the organization
- Ensuring colleagues are aware of the importance of risk management and compliance
### Marketing
- **Purpose:** Respellion has an outstanding corporate reputation that alligns with the core values and manifest.
- **Filler:** Raymond Verhoef
- **Accountabilities:**
- Maintaining our exposure on social media
- Maintaining the website on functional level
- Developing relevant exposure on the website
- Improving the corporate reputation in a structured and managed way
### Networker
- **Purpose:** Maintain a valuable and effective network of stakeholders.
- **Filler:** Patrick Smulders
- **Accountabilities:**
- Maintaining a network of partners (ICT brokers, IT companies)
- Maintaining a network of potential customers
- Monitoring a transparent sales funnel for potential tenders
- Monitoring a transparent sales funnel for potential individual assignments
- Aligning the sales funnel with the financial budget administration and forecast
- Being transparent (communication) about the accountabilities above
### People Officer
- **Purpose:** Making sure Respellion has/maintains the organizational culture in which individuals can be the best versions of themselves. And making sure Respellion has/maintains an unique and attractive organizational structure.
- **Filler:** Raymond Verhoef
- **Domains:** An asset, process, or other thing this Role may exclusively control and regulate as its property, for its purpose.
- **Accountabilities:**
- Maintaining the performance model (salary etc)
- Facilitating the monthly celebration meeting
- Guarding the Respellion culture/DNA
- Designing employment contracts
- Maintaining an effective onboarding program
- Approving leave requests
- Maintaining the employee handbook
- Maintaining a comprehensive sick leave administration
- Maintaining a comprehensive leave administration
- Taking initiatives to improve people wellbeing
- Increasing the connection with people's social system at home
- Making sure that successes are being celebrated
### Privacy Officer
- **Purpose:** To ensure Respellion maintains the highest standards of data privacy compliance while enabling the organization to effectively use data in alignment with our values of trust, courage, self-discipline, and entrepreneurship.
- **Filler:** Jos van Aalderen
- **Accountabilities:**
- Developing, implementing, and maintaining a comprehensive privacy program aligned with GDPR, Dutch privacy laws, and other applicable regulations
- Monitoring and interpreting changes in privacy legislation and updating organizational policies accordingly
- Serving as the primary point of contact for supervisory authorities (like the Dutch Data Protection Authority)
- Conducting regular privacy impact assessments for new and existing processing activities
- Maintaining records of processing activities as required by Article 30 of GDPR
- Identifying privacy risks across the organization and recommending appropriate risk mitigation strategies
- Managing privacy incident response, including breach notification procedures
- Reviewing data processing agreements with vendors and partners to ensure privacy requirements are adequately addressed
- Developing and delivering privacy training programs for all team members
- Establishing and overseeing processes for handling data subject rights requests (access, rectification, erasure, etc.)
- Ensuring timely responses to privacy inquiries from customers, employees, and other stakeholders
### Process guardian
- **Purpose:** Making sure Respellions organizational processes are transparant, compliant and scalable.
- **Filler:** Raymond Verhoef
- **Accountabilities:**
- Maintaining a file structure for documents that facilitates compliance and transparancy
- Maintaining a comprehensive quality management system that complies with potentional ISO regulations
- Advising the circle about effective software platforms
- Advising the circle about (auditing) the quality management system(s)
- Advising the circle about any other relevant internal processes
- Organizing internal audits as described in ISO9001:2015 chapter 9
### Recruiter
- **Purpose:** Recruiting new employees who fit the Respellion's culture.
- **Fillers:** Jos van Aalderen, Patrick Smulders
- **Accountabilities:**
- Selecting potential employees via necessary channels
- Running the first telephone conversations in the application process
- Organizing the introduction interview of the application process
- Organizing the capacity interview of the application process
- Monitoring the application process
- Placing job vacancies on external websites
- Making sure the application process is done within two weeks
- Using a LinkedIn recruiter subscription
- Placing posts for Respellion employer branding
- Drafting job descriptions to attract potential employees
- Reporting regularly on the recruiting process