Add comprehensive documentation for key organizational aspects
- 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:
@@ -1,407 +1,86 @@
|
||||
# Curriculum service spec
|
||||
# Curriculum spec: 26-week per-user cycle
|
||||
|
||||
## Responsibility
|
||||
|
||||
Generates a versioned 26-week learning schedule from the published knowledge
|
||||
base. Manages perpetual cycling, version transitions, and employee curriculum
|
||||
state. Handles regeneration when the KB changes.
|
||||
The curriculum sequences the knowledge base into a 26-week schedule. Every
|
||||
employee runs their **own** cycle, starting when they enroll. Implemented in
|
||||
`src/lib/curriculumService.js`; admin UI in
|
||||
`src/components/admin/CurriculumManager.jsx`.
|
||||
|
||||
---
|
||||
|
||||
## Service location
|
||||
## Data
|
||||
|
||||
```
|
||||
app/services/curriculum/
|
||||
├── src/
|
||||
│ ├── index.ts entry point, Fastify server
|
||||
│ ├── routes/
|
||||
│ │ ├── curriculum.ts POST /generate, GET /current, GET /preview
|
||||
│ │ └── 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
|
||||
```
|
||||
- **`curriculum_versions`** — generated schedules. Lifecycle `draft → active →
|
||||
superseded`; exactly one `active`. The `schedule` JSON is an array of 26 week
|
||||
objects: `{ week_number (1–26), theme, topic_ids[], estimated_duration (15–45),
|
||||
week_rationale }`. `coverage_stats` records theme/topic coverage.
|
||||
- **`topics`** — supply `theme`, `complexity_weight` (1–5), and `difficulty` as
|
||||
generation input.
|
||||
|
||||
---
|
||||
|
||||
## API surface
|
||||
## Topic enrichment (prerequisite)
|
||||
|
||||
### POST /generate
|
||||
|
||||
Triggers curriculum generation from current published KB.
|
||||
Called by admin app after confirming regeneration.
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"triggeredBy": "string",
|
||||
"reason": "new_topics" | "manual"
|
||||
}
|
||||
```
|
||||
|
||||
Response (202 Accepted):
|
||||
```json
|
||||
{
|
||||
"jobId": "string",
|
||||
"status": "queued"
|
||||
}
|
||||
```
|
||||
Generation needs themed, weighted topics. `enrichTopicsForCurriculum()` finds
|
||||
topics missing a `theme` (excluding `type='fact'` and `learning_relevance='exclude'`)
|
||||
and, in batches of 20, calls Claude with `emit_topic_enrichment` to assign
|
||||
`theme`, `complexity_weight`, and `difficulty`. Triggered from the Curriculum tab.
|
||||
|
||||
---
|
||||
|
||||
### GET /preview
|
||||
## Generation
|
||||
|
||||
Returns proposed new curriculum before admin confirms.
|
||||
Called by admin app to show preview before regeneration is applied.
|
||||
`generateCurriculumDraft(reason)`:
|
||||
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 15–45, 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:
|
||||
```json
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
```
|
||||
`confirmVersion(versionId, adminUserId)` activates a draft (and supersedes the old
|
||||
active version); `rejectVersion` discards a draft.
|
||||
|
||||
---
|
||||
|
||||
### 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.
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"userId": "string",
|
||||
"currentCycle": 1,
|
||||
"currentWeek": 7,
|
||||
"startDate": "2026-01-15T00:00:00Z",
|
||||
"activeVersionId": "string",
|
||||
"nextSessionTheme": { "id": "string", "title": "string" },
|
||||
"nextSessionTopics": []
|
||||
}
|
||||
```
|
||||
- `getCurrentWeekContent(personalWeek)` reads the active version's schedule, maps the
|
||||
26-week slot to its `topic_ids`, and returns `{ cycle, weekNumber, theme, topics,
|
||||
estimatedDuration, rationale }`.
|
||||
- `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
|
||||
fallback.**
|
||||
- `getYearProgress(userId, personalWeek)` computes completion for the current cycle.
|
||||
|
||||
---
|
||||
|
||||
### POST /advance/:userId
|
||||
## Notes
|
||||
|
||||
Called by progress service when an employee completes a week.
|
||||
Increments currentWeek, handles cycle transition at week 26.
|
||||
|
||||
Request:
|
||||
```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 // 1–5
|
||||
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 (15–45 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 // 1–26
|
||||
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 1–5 unchanged, weeks 6–26 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
|
||||
- There is no shared "current week" and no `admin:current_week` setting.
|
||||
- Regeneration produces a new version; activating it changes future weeks for all
|
||||
users. Completion history (`micro_learning_completions`) is append-only and never
|
||||
rewritten.
|
||||
|
||||
Reference in New Issue
Block a user