TypeScript 98.3%
CSS 0.9%
Shell 0.7%
1// Amorçage de la plateforme : cours, compte administrateur initial (hash bcrypt + changement2// forcé), prompts versionnés, concepts/cartes/questions (seed-data/*.json), examens blancs.3// Idempotent : réexécutable sans dupliquer.45import { existsSync, readFileSync } from "node:fs";6import { resolve } from "node:path";7import { all, db, get, getSetting, run, setSetting, transaction } from "../lib/db/index.ts";8import { hashPassword } from "../lib/auth/password.ts";9import { seedPromptsFromFiles } from "../lib/prompts.ts";1011db();1213// ------------------------------ Cours ------------------------------14const COURSES = [15 {16 code: "IMM1003",17 full_code: "IMM1003-20",18 title: "Éléments d'évaluation immobilière",19 session_label: "Automne 2026",20 description:21 "Fondements de l'évaluation immobilière au Québec : cadre professionnel OEAQ, principes économiques, types de valeur, marché québécois, méthodes de comparaison, du coût et du revenu, réconciliation et rapport d'évaluation.",22 color: "#003E7E",23 source_path: process.env.COURSE_IMM1003_PATH || "../IMM1003-20",24 },25 {26 code: "IMM1033",27 full_code: "IMM1033-20",28 title: "Méthodes du coût en évaluation immobilière",29 session_label: "Automne 2026",30 description:31 "Maîtrise complète de la méthode du coût : évaluation du terrain, estimation du coût de reproduction ou de remplacement, coûts directs et indirects, profit de l'entrepreneur, mesure des trois formes de dépréciation et applications spécialisées.",32 color: "#B45309",33 source_path: process.env.COURSE_IMM1033_PATH || "../IMM1033-20",34 },35];3637for (const c of COURSES) {38 run(39 `INSERT INTO courses (code, full_code, title, session_label, description, color, source_path)40 VALUES (?, ?, ?, ?, ?, ?, ?)41 ON CONFLICT(code) DO UPDATE SET full_code=excluded.full_code, title=excluded.title,42 session_label=excluded.session_label, description=excluded.description,43 color=excluded.color, source_path=excluded.source_path`,44 c.code, c.full_code, c.title, c.session_label, c.description, c.color, c.source_path45 );46}47console.log(`✓ ${COURSES.length} cours`);4849// ------------------------------ Admin initial ------------------------------50if (process.env.DISABLE_INITIAL_ADMIN === "true") {51 console.log("• Compte admin initial désactivé (DISABLE_INITIAL_ADMIN=true)");52} else {53 const username = process.env.INITIAL_ADMIN_USERNAME || "admin";54 const password = process.env.INITIAL_ADMIN_PASSWORD || "admin123";55 const force = (process.env.FORCE_INITIAL_ADMIN_PASSWORD_CHANGE ?? "true") !== "false";56 const existing = get<{ id: number }>("SELECT id FROM users WHERE username = ?", username);57 if (!existing) {58 const r = run(59 `INSERT INTO users (username, display_name, password_hash, role, must_change_password, is_initial_admin)60 VALUES (?, 'Administrateur', ?, 'admin', ?, 1)`,61 username, hashPassword(password), force ? 1 : 062 );63 const adminId = Number(r.lastInsertRowid);64 for (const c of COURSES) run("INSERT OR IGNORE INTO enrollments (user_id, course_code) VALUES (?, ?)", adminId, c.code);65 console.log(`✓ Compte administrateur initial « ${username} » créé (changement de mot de passe ${force ? "FORCÉ à la première connexion" : "non forcé"})`);66 } else {67 console.log(`• Compte « ${username} » déjà présent — aucun changement`);68 }69}7071// ------------------------------ Prompts ------------------------------72seedPromptsFromFiles();73console.log(`✓ Prompts versionnés (${all("SELECT DISTINCT name FROM prompt_versions").length} noms)`);7475// ------------------------------ Contenu pédagogique ------------------------------76type ConceptSeed = { slug: string; name: string; description: string; week: number; importance: number; axis: string };77type LinkSeed = { from: string; to: string; type: string };78type CardSeed = { concept: string; type: string; front: string; back: string };79type QuestionSeed = { concept: string; type: string; difficulty: number; question: string; options: string[]; answer: string; explanation: string };8081function loadJSON<T>(file: string): T | null {82 const p = resolve(process.cwd(), "seed-data", file);83 if (!existsSync(p)) return null;84 return JSON.parse(readFileSync(p, "utf8")) as T;85}8687for (const course of COURSES) {88 const key = course.code.toLowerCase();89 const conceptsData = loadJSON<{ concepts: ConceptSeed[]; links: LinkSeed[] }>(`${key}-concepts.json`);90 if (!conceptsData) {91 console.log(`⚠ seed-data/${key}-concepts.json absent — contenu pédagogique de ${course.code} non amorcé`);92 continue;93 }94 transaction(() => {95 for (const c of conceptsData.concepts) {96 run(97 `INSERT INTO concepts (course_code, slug, name, description, week, importance, axis)98 VALUES (?, ?, ?, ?, ?, ?, ?)99 ON CONFLICT(course_code, slug) DO UPDATE SET name=excluded.name, description=excluded.description,100 week=excluded.week, importance=excluded.importance, axis=excluded.axis`,101 course.code, c.slug, c.name, c.description, c.week, c.importance, c.axis102 );103 }104 const idOf = new Map(105 all<{ id: number; slug: string }>("SELECT id, slug FROM concepts WHERE course_code = ?", course.code).map((r) => [r.slug, r.id])106 );107 for (const l of conceptsData.links ?? []) {108 const from = idOf.get(l.from), to = idOf.get(l.to);109 if (from && to) run("INSERT OR IGNORE INTO concept_links (from_id, to_id, type) VALUES (?, ?, ?)", from, to, l.type);110 }111112 const cards = loadJSON<{ flashcards: CardSeed[] }>(`${key}-flashcards.json`);113 if (cards) {114 const already = get<{ n: number }>("SELECT COUNT(*) as n FROM flashcards WHERE course_code = ? AND created_by = 'seed'", course.code);115 if ((already?.n ?? 0) === 0) {116 for (const f of cards.flashcards) {117 run(118 `INSERT INTO flashcards (course_code, concept_id, type, front, back, created_by, validated)119 VALUES (?, ?, ?, ?, ?, 'seed', 1)`,120 course.code, idOf.get(f.concept) ?? null, f.type, f.front, f.back121 );122 }123 console.log(`✓ ${course.code} : ${cards.flashcards.length} cartes mémoire`);124 }125 }126127 const quiz = loadJSON<{ questions: QuestionSeed[] }>(`${key}-quiz.json`);128 if (quiz) {129 const already = get<{ n: number }>("SELECT COUNT(*) as n FROM quiz_questions WHERE course_code = ? AND created_by = 'seed'", course.code);130 if ((already?.n ?? 0) === 0) {131 for (const q of quiz.questions) {132 run(133 `INSERT INTO quiz_questions (course_code, concept_id, type, difficulty, question, options, answer, explanation, created_by, validated)134 VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'seed', 1)`,135 course.code, idOf.get(q.concept) ?? null, q.type, q.difficulty, q.question, JSON.stringify(q.options ?? []), q.answer, q.explanation136 );137 }138 console.log(`✓ ${course.code} : ${quiz.questions.length} questions de quiz`);139 }140 }141 });142 console.log(`✓ ${course.code} : ${conceptsData.concepts.length} concepts, ${(conceptsData.links ?? []).length} liens`);143}144145// Lien inter-cours : la méthode du coût d'IMM1003 (survol) est approfondie par IMM1033.146const c1003cout = get<{ id: number }>(147 "SELECT id FROM concepts WHERE course_code = 'IMM1003' AND (slug LIKE '%cout%' OR slug LIKE '%coût%') ORDER BY importance DESC LIMIT 1"148);149const c1033roots = all<{ id: number }>(150 "SELECT id FROM concepts WHERE course_code = 'IMM1033' AND week <= 2 ORDER BY importance DESC LIMIT 2"151);152if (c1003cout && c1033roots.length) {153 for (const r of c1033roots) {154 run("INSERT OR IGNORE INTO concept_links (from_id, to_id, type) VALUES (?, ?, 'approfondissement')", r.id, c1003cout.id);155 }156}157158// ------------------------------ Examens blancs ------------------------------159type ExamSpec = { course: string; kind: string; title: string; description: string; duration: number; weeks: [number, number]; n: number };160const EXAMS: ExamSpec[] = [161 { course: "IMM1003", kind: "intra", title: "Intra blanc — Fondements et marché (S1-6)", description: "Simulation de l'examen de mi-session : fondements, cadre professionnel, principes, valeur, marché, collecte de données.", duration: 120, weeks: [1, 6], n: 18 },162 { course: "IMM1003", kind: "final", title: "Final blanc — Les trois méthodes (S1-13, accent 8-13)", description: "Simulation de l'examen final : méthodes de comparaison, du coût et du revenu, réconciliation, rapport.", duration: 170, weeks: [1, 13], n: 24 },163 { course: "IMM1003", kind: "thematic", title: "Examen thématique — Méthode du revenu", description: "RBP→RNE, capitalisation directe, TGA, DCF, multiplicateurs.", duration: 75, weeks: [11, 12], n: 12 },164 { course: "IMM1033", kind: "intra", title: "Intra blanc — Terrain et coûts (S1-8)", description: "Simulation de l'intra : fondements, évaluation du terrain, estimation des coûts directs et indirects.", duration: 120, weeks: [1, 8], n: 18 },165 { course: "IMM1033", kind: "final", title: "Final blanc — Dépréciation et intégration (S1-14)", description: "Simulation du final : les trois dépréciations, applications spécialisées, cas intégrateur.", duration: 170, weeks: [1, 14], n: 24 },166 { course: "IMM1033", kind: "thematic", title: "Examen thématique — Les trois dépréciations", description: "Physique, fonctionnelle, économique : classification, mesure, non-double-comptage.", duration: 75, weeks: [9, 12], n: 12 },167];168169for (const e of EXAMS) {170 const exists = get("SELECT id FROM mock_exams WHERE course_code = ? AND kind = ? AND title = ?", e.course, e.kind, e.title);171 if (exists) continue;172 // Sélection pondérée : questions des semaines visées, difficultés étalées, accent sur la fin de portée pour les finaux.173 const qs = all<{ id: number; difficulty: number; week: number | null }>(174 `SELECT q.id, q.difficulty, c.week FROM quiz_questions q175 LEFT JOIN concepts c ON c.id = q.concept_id176 WHERE q.course_code = ? AND (c.week IS NULL OR (c.week >= ? AND c.week <= ?))177 ORDER BY RANDOM()`,178 e.course, e.weeks[0], e.weeks[1]179 );180 if (qs.length < Math.min(8, e.n)) {181 console.log(`⚠ ${e.title} : banque insuffisante (${qs.length} questions) — examen non créé`);182 continue;183 }184 // Étaler les difficultés : trier par difficulté et prendre en alternance.185 const sorted = [...qs].sort((a, b) => a.difficulty - b.difficulty);186 const picked: number[] = [];187 const step = sorted.length / Math.min(e.n, sorted.length);188 for (let i = 0; i < Math.min(e.n, sorted.length); i++) picked.push(sorted[Math.floor(i * step)].id);189 run(190 `INSERT INTO mock_exams (course_code, kind, title, description, duration_minutes, question_ids, config, created_by, official)191 VALUES (?, ?, ?, ?, ?, ?, ?, 'seed', 1)`,192 e.course, e.kind, e.title, e.description, e.duration, JSON.stringify(picked), JSON.stringify({ weeks: e.weeks })193 );194 console.log(`✓ Examen blanc : ${e.title} (${picked.length} questions)`);195}196197// ------------------------------ Paramètres par défaut ------------------------------198if (getSetting("integrity_policy", null) === null) {199 setSetting("integrity_policy", { enabled: true, examLockdown: false, allowedModesDuringLockdown: ["tutor", "socratic"] });200}201if (getSetting("cross_course_enabled", null) === null) setSetting("cross_course_enabled", false);202if (getSetting("sharing_enabled", null) === null) setSetting("sharing_enabled", true);203console.log("✓ Paramètres par défaut");204205const counts = {206 users: get<{ n: number }>("SELECT COUNT(*) as n FROM users")?.n,207 concepts: get<{ n: number }>("SELECT COUNT(*) as n FROM concepts")?.n,208 flashcards: get<{ n: number }>("SELECT COUNT(*) as n FROM flashcards")?.n,209 questions: get<{ n: number }>("SELECT COUNT(*) as n FROM quiz_questions")?.n,210 exams: get<{ n: number }>("SELECT COUNT(*) as n FROM mock_exams")?.n,211};212console.log("— Seed terminé :", JSON.stringify(counts));213