@mostajs/elearning 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,4 @@
1
+ # Changelog — @mostajs/elearning
2
+ ## [0.1.0] — 2026-06-18
3
+ ### Added
4
+ - Leçons (video/pdf/text/quiz) + progression (start/complete, courseProgress %, nextLesson). Compose media/storage/questa. 5 tests + exemple §12.
package/README.md ADDED
@@ -0,0 +1,11 @@
1
+ # @mostajs/elearning
2
+ **Auteur** : Dr Hamid MADANI <drmdh@msn.com> · **Licence** : AGPL-3.0-or-later · **Statut** : 0.1.0 (5 tests verts)
3
+ > LMS : leçons (vidéo/PDF/quiz) + progression. Autonome ; compose media/storage/questa/gradebook ; délègue catalogue à @mostajs/training.
4
+ ```js
5
+ import { createElearning, createMemoryRepositories } from '@mostajs/elearning';
6
+ const e = createElearning({ repositories: createMemoryRepositories(), media, storage });
7
+ await e.content.addLesson('eng-a1', { order:1, kind:'video', title:'Intro', mediaRef:'vid-1' });
8
+ await e.progress.complete(learnerId, lessonId, { score: 18 });
9
+ const cp = await e.progress.courseProgress(learnerId, 'eng-a1'); // { total, done, percent }
10
+ ```
11
+ Lancer : `node test-scripts/unit/elearning.test.mjs && node examples/parcours/run.mjs`
@@ -0,0 +1,10 @@
1
+ # @mostajs/elearning — DEVRULES de bout en bout (§4 + #3 + #4 condensés)
2
+ **Auteur** : Dr Hamid MADANI <drmdh@msn.com> · **Date** : 2026-06-18 · **Statut** : cas C (P2 ATC + P1 formation incubés)
3
+ ## §4 (règle d'or)
4
+ `@mostajs/media`=vidéo/capture · `storage`=PDF · `questa`=quiz · `gradebook`=notes · `training`=catalogue/sessions.
5
+ → elearning = la **couche contenu+progression** (leçons, parcours) qui **compose** ces briques. Frontière : training=quoi/qui ; elearning=le contenu et l'avancement.
6
+ ## §3 (périmètre/API)
7
+ **Entités** : Lesson{courseId,order,kind(video/pdf/text/quiz/live),mediaRef,contentRef,quizRef,durationMin}, LessonProgress{learnerId,lessonId,status,score}.
8
+ **API** : `createElearning({repositories,media?,storage?})` → `content.{addLesson,list,get}`, `progress.{start,complete,get,courseProgress,nextLesson}`.
9
+ **Compose (§10)** : media (vidéo), storage (PDF), questa (quiz→score), gradebook (note). **Jalons** : 0.1 leçons+progression · 0.2 quiz(questa)/vidéo(media) · 1.0 14 livrables.
10
+ ## §4 (tests) : addLesson ordonné, start/complete, courseProgress %, nextLesson, leçon introuvable. (5 verts)
@@ -0,0 +1,12 @@
1
+ import assert from 'node:assert/strict';
2
+ import { createElearning, createMemoryRepositories } from '../../src/index.js';
3
+ const e=createElearning({ repositories:createMemoryRepositories() });
4
+ const c='eng-a1';
5
+ await e.content.addLesson(c,{order:1,title:'Vidéo intro',kind:'video',mediaRef:'vid-1',durationMin:8});
6
+ await e.content.addLesson(c,{order:2,title:'Support PDF',kind:'pdf',contentRef:'pdf-1'});
7
+ await e.content.addLesson(c,{order:3,title:'Quiz A1',kind:'quiz',quizRef:'quiz-1'});
8
+ await e.progress.complete('ines',(await e.content.list(c))[0].id);
9
+ const next=await e.progress.nextLesson('ines',c);
10
+ const cp=await e.progress.courseProgress('ines',c);
11
+ console.log('✅ elearning — Inès : %d%% · prochaine leçon: %s', cp.percent, next.title);
12
+ assert.equal(cp.percent,33);
package/llms.txt ADDED
@@ -0,0 +1,11 @@
1
+ # @mostajs/elearning — fiche LLM
2
+ RÔLE
3
+ LMS : leçons/contenu (video/pdf/text/quiz/live) + progression par apprenant. Autonome/DB-agnostique.
4
+ Compose (injectés) : media (vidéo), storage (PDF), questa (quiz), gradebook (score). Délègue catalogue/sessions à @mostajs/training.
5
+ EXPORTS
6
+ createElearning({ repositories, media?, storage?, now? }) -> { content, progress } ; createMemoryRepositories(); LessonSchema, ProgressSchema
7
+ API
8
+ content.addLesson(courseId,{order,kind,title,mediaRef|contentRef|quizRef})/list(courseId)/get(id)
9
+ progress.start(learnerId,lessonId)/complete(learnerId,lessonId,{score})/get/courseProgress(learnerId,courseId)->{total,done,percent}/nextLesson(learnerId,courseId)
10
+ PIÈGES
11
+ - courseId/learnerId/mediaRef opaques (→ training/users/media). nextLesson = 1ère leçon non 'done' (ordre). Le quiz/score = compose questa/gradebook.
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@mostajs/elearning",
3
+ "version": "0.1.0",
4
+ "description": "LMS : leçons/contenu (vidéo/PDF/texte/quiz) + progression par apprenant. DB-agnostique ; compose media/storage/questa/gradebook. Délègue catalogue à @mostajs/training.",
5
+ "license": "AGPL-3.0-or-later",
6
+ "author": "Dr Hamid MADANI <drmdh@msn.com>",
7
+ "type": "module",
8
+ "main": "src/index.js",
9
+ "exports": {
10
+ ".": "./src/index.js",
11
+ "./schemas": "./src/schemas.js"
12
+ },
13
+ "keywords": [
14
+ "mostajs",
15
+ "elearning",
16
+ "lms",
17
+ "lessons",
18
+ "progress",
19
+ "video"
20
+ ],
21
+ "scripts": {
22
+ "test": "node test-scripts/unit/elearning.test.mjs",
23
+ "example": "node examples/parcours/run.mjs"
24
+ }
25
+ }
@@ -0,0 +1,23 @@
1
+ // @mostajs/elearning — contenu + progression. Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ // Autonome/DB-agnostique. Compose (injectés) : media (vidéo), storage (PDF), questa (quiz), gradebook (score). Délègue catalogue à training.
3
+ export function createElearning({ repositories, media, storage, now=()=>new Date() }={}){
4
+ if(!repositories?.lessons) throw new Error('createElearning: repositories.lessons requis');
5
+ const { lessons, progress } = repositories;
6
+ const findP=async(l,les)=>(await progress.find(p=>p.learnerId===l && p.lessonId===les))[0];
7
+ const api={
8
+ content:{
9
+ addLesson:(courseId,dto)=>lessons.create({courseId,order:0,kind:'text',...dto}),
10
+ list:(courseId)=>lessons.find(x=>x.courseId===courseId).then(a=>a.sort((x,y)=>x.order-y.order)),
11
+ get:(id)=>lessons.findById(id),
12
+ },
13
+ progress:{
14
+ async start(learnerId,lessonId){ const les=await lessons.findById(lessonId); if(!les) throw new Error('leçon introuvable'); const ex=await findP(learnerId,lessonId); return ex?progress.update(ex.id,{status:'in_progress',at:now()}):progress.create({learnerId,lessonId,courseId:les.courseId,status:'in_progress',at:now()}); },
15
+ async complete(learnerId,lessonId,{score}={}){ const les=await lessons.findById(lessonId); const ex=await findP(learnerId,lessonId); const d={status:'done',score:score??null,at:now()}; return ex?progress.update(ex.id,d):progress.create({learnerId,lessonId,courseId:les?.courseId,...d}); },
16
+ get:(learnerId,lessonId)=>findP(learnerId,lessonId),
17
+ async courseProgress(learnerId,courseId){ const all=await lessons.find(l=>l.courseId===courseId); const done=(await progress.find(p=>p.learnerId===learnerId && p.courseId===courseId && p.status==='done')).length; return { total:all.length, done, percent: all.length?Math.round(done/all.length*100):0 }; },
18
+ async nextLesson(learnerId,courseId){ const all=(await lessons.find(l=>l.courseId===courseId)).sort((a,b)=>a.order-b.order); for(const l of all){ const p=await findP(learnerId,l.id); if(!p||p.status!=='done') return l; } return null; },
19
+ },
20
+ repositories,
21
+ };
22
+ return api;
23
+ }
package/src/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { createElearning } from './elearning.js';
2
+ export { createMemoryRepositories } from './memory-repo.js';
3
+ export { LessonSchema, ProgressSchema } from './schemas.js';
@@ -0,0 +1,7 @@
1
+ // @mostajs/elearning — repos mémoire. Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ function coll(){ const m=new Map(); return {
3
+ async create(d){ const id=d.id||globalThis.crypto.randomUUID(); const now=new Date(); const r={id,createdAt:now,updatedAt:now,...d}; m.set(id,r); return {...r}; },
4
+ async findById(id){ const r=m.get(id); return r?{...r}:null; },
5
+ async update(id,p){ const r=m.get(id); if(!r)return null; const x={...r,...p,updatedAt:new Date()}; m.set(id,x); return {...x}; },
6
+ async find(f=()=>true){ return [...m.values()].filter(f).map(r=>({...r})); } }; }
7
+ export function createMemoryRepositories(){ return { lessons:coll(), progress:coll() }; }
package/src/schemas.js ADDED
@@ -0,0 +1,10 @@
1
+ // @mostajs/elearning — schémas. Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ export const LessonSchema = { name:'Lesson', collection:'lessons', timestamps:true, fields:{
3
+ courseId:{type:'string',required:true}, order:{type:'number',default:0}, title:{type:'string',required:true},
4
+ kind:{type:'string',enum:['video','pdf','text','quiz','live'],default:'text'},
5
+ mediaRef:{type:'string',default:null}, contentRef:{type:'string',default:null}, quizRef:{type:'string',default:null},
6
+ durationMin:{type:'number',default:0} }, indexes:[{fields:{courseId:'asc'}}] };
7
+ export const ProgressSchema = { name:'LessonProgress', collection:'lesson_progress', timestamps:true, fields:{
8
+ learnerId:{type:'string',required:true}, lessonId:{type:'string',required:true}, courseId:{type:'string'},
9
+ status:{type:'string',enum:['not_started','in_progress','done'],default:'not_started'}, score:{type:'number',default:null}, at:{type:'date',default:'now'} },
10
+ indexes:[{fields:{learnerId:'asc'}},{fields:{lessonId:'asc'}}] };
@@ -0,0 +1,10 @@
1
+ import assert from 'node:assert/strict';
2
+ import { createElearning, createMemoryRepositories } from '../../src/index.js';
3
+ let pass=0; const test=async(n,f)=>{await f();pass++;console.log(' ✓',n);};
4
+ const mk=()=>createElearning({ repositories:createMemoryRepositories() });
5
+ await test('addLesson + list ordonné', async()=>{ const e=mk(); await e.content.addLesson('c',{order:2,title:'L2'}); await e.content.addLesson('c',{order:1,title:'L1',kind:'video',mediaRef:'m1'}); assert.deepEqual((await e.content.list('c')).map(l=>l.title),['L1','L2']); });
6
+ await test('start/complete + get', async()=>{ const e=mk(); const l=await e.content.addLesson('c',{title:'L'}); await e.progress.start('u',l.id); await e.progress.complete('u',l.id,{score:18}); const p=await e.progress.get('u',l.id); assert.equal(p.status,'done'); assert.equal(p.score,18); });
7
+ await test('courseProgress %', async()=>{ const e=mk(); const a=await e.content.addLesson('c',{title:'A'}); await e.content.addLesson('c',{title:'B'}); await e.progress.complete('u',a.id); const cp=await e.progress.courseProgress('u','c'); assert.equal(cp.total,2); assert.equal(cp.done,1); assert.equal(cp.percent,50); });
8
+ await test('nextLesson (1ère non terminée)', async()=>{ const e=mk(); const a=await e.content.addLesson('c',{order:1,title:'A'}); const b=await e.content.addLesson('c',{order:2,title:'B'}); await e.progress.complete('u',a.id); assert.equal((await e.progress.nextLesson('u','c')).id,b.id); await e.progress.complete('u',b.id); assert.equal(await e.progress.nextLesson('u','c'),null); });
9
+ await test('leçon introuvable au start', async()=>{ await assert.rejects(()=>mk().progress.start('u','nope'),/introuvable/); });
10
+ console.log(`\n✅ @mostajs/elearning — ${pass} tests OK`);